mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 01:19:19 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5422f7d6af |
@@ -1,33 +0,0 @@
|
|||||||
name: Add Contributors Label
|
|
||||||
|
|
||||||
on:
|
|
||||||
# issues:
|
|
||||||
# types: [opened]
|
|
||||||
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
add-contributor-label:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Add Contributor Label
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const isPR = !!context.payload.pull_request;
|
|
||||||
const issueNumber = isPR ? context.payload.pull_request.number : context.payload.issue.number;
|
|
||||||
const authorAssociation = isPR ? context.payload.pull_request.author_association : context.payload.issue.author_association;
|
|
||||||
|
|
||||||
if (authorAssociation === 'CONTRIBUTOR') {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
labels: ['contributor']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+154
-171
@@ -4,7 +4,6 @@ run-name: "${{ format('release {0}', inputs.bump) }}"
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- ci
|
|
||||||
- dev
|
- dev
|
||||||
- snapshot-*
|
- snapshot-*
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -30,177 +29,21 @@ permissions:
|
|||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version:
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
if: github.repository == 'anomalyco/opencode'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
- id: version
|
|
||||||
run: |
|
|
||||||
./script/version.ts
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
|
||||||
OPENCODE_VERSION: ${{ inputs.version }}
|
|
||||||
outputs:
|
|
||||||
version: ${{ steps.version.outputs.version }}
|
|
||||||
release: ${{ steps.version.outputs.release }}
|
|
||||||
tag: ${{ steps.version.outputs.tag }}
|
|
||||||
|
|
||||||
build-cli:
|
|
||||||
needs: version
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
if: github.repository == 'anomalyco/opencode'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
fetch-tags: true
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
id: build
|
|
||||||
run: |
|
|
||||||
./packages/opencode/script/build.ts
|
|
||||||
env:
|
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
|
||||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: opencode-cli
|
|
||||||
path: packages/opencode/dist
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
version: ${{ needs.version.outputs.version }}
|
|
||||||
|
|
||||||
build-tauri:
|
|
||||||
needs:
|
|
||||||
- build-cli
|
|
||||||
- version
|
|
||||||
continue-on-error: false
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
settings:
|
|
||||||
- host: macos-latest
|
|
||||||
target: x86_64-apple-darwin
|
|
||||||
- host: macos-latest
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
- host: blacksmith-4vcpu-windows-2025
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
- host: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
- host: blacksmith-4vcpu-ubuntu-2404-arm
|
|
||||||
target: aarch64-unknown-linux-gnu
|
|
||||||
runs-on: ${{ matrix.settings.host }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
fetch-tags: true
|
|
||||||
|
|
||||||
- uses: apple-actions/import-codesign-certs@v2
|
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
with:
|
|
||||||
keychain: build
|
|
||||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Verify Certificate
|
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
run: |
|
|
||||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
|
||||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
|
||||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
|
||||||
echo "Certificate imported."
|
|
||||||
|
|
||||||
- name: Setup Apple API Key
|
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: install dependencies (ubuntu only)
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ matrix.settings.target }}
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: packages/desktop/src-tauri
|
|
||||||
shared-key: ${{ matrix.settings.target }}
|
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
cd packages/desktop
|
|
||||||
bun ./scripts/prepare.ts
|
|
||||||
env:
|
|
||||||
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
|
||||||
RUST_TARGET: ${{ matrix.settings.target }}
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
|
||||||
|
|
||||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
|
||||||
- name: Install tauri-cli from portable appimage branch
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
run: |
|
|
||||||
cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
|
|
||||||
echo "Installed tauri-cli version:"
|
|
||||||
cargo tauri --version
|
|
||||||
|
|
||||||
- name: Build and upload artifacts
|
|
||||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
|
||||||
timeout-minutes: 60
|
|
||||||
with:
|
|
||||||
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
|
|
||||||
updaterJsonPreferNsis: true
|
|
||||||
releaseId: ${{ needs.version.outputs.release }}
|
|
||||||
tagName: ${{ needs.version.outputs.tag }}
|
|
||||||
releaseDraft: true
|
|
||||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_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 }}
|
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
|
||||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
||||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
|
||||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs:
|
|
||||||
- version
|
|
||||||
- build-cli
|
|
||||||
- build-tauri
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
if: github.repository == 'anomalyco/opencode'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: git fetch --force --tags
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- name: Install OpenCode
|
- name: Install OpenCode
|
||||||
if: inputs.bump || inputs.version
|
if: inputs.bump || inputs.version
|
||||||
run: bun i -g opencode-ai
|
run: bun i -g opencode-ai@1.0.169
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -226,13 +69,156 @@ jobs:
|
|||||||
git config --global user.name "opencode"
|
git config --global user.name "opencode"
|
||||||
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- name: Publish
|
||||||
|
id: publish
|
||||||
|
run: ./script/publish-start.ts
|
||||||
|
env:
|
||||||
|
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||||
|
OPENCODE_VERSION: ${{ inputs.version }}
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||||
|
NPM_CONFIG_PROVENANCE: false
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
release: ${{ steps.publish.outputs.release }}
|
||||||
|
tag: ${{ steps.publish.outputs.tag }}
|
||||||
|
version: ${{ steps.publish.outputs.version }}
|
||||||
|
|
||||||
|
publish-tauri:
|
||||||
|
needs: publish
|
||||||
|
continue-on-error: false
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
settings:
|
||||||
|
- host: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
- host: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
- host: blacksmith-4vcpu-windows-2025
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
- host: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
- host: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
runs-on: ${{ matrix.settings.host }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ needs.publish.outputs.tag }}
|
||||||
|
|
||||||
|
- uses: apple-actions/import-codesign-certs@v2
|
||||||
|
if: ${{ runner.os == 'macOS' }}
|
||||||
|
with:
|
||||||
|
keychain: build
|
||||||
|
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Verify Certificate
|
||||||
|
if: ${{ runner.os == 'macOS' }}
|
||||||
|
run: |
|
||||||
|
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||||
|
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||||
|
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||||
|
echo "Certificate imported."
|
||||||
|
|
||||||
|
- name: Setup Apple API Key
|
||||||
|
if: ${{ runner.os == 'macOS' }}
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||||
|
|
||||||
|
- run: git fetch --force --tags
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: install dependencies (ubuntu only)
|
||||||
|
if: contains(matrix.settings.host, 'ubuntu')
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.settings.target }}
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: packages/desktop/src-tauri
|
||||||
|
shared-key: ${{ matrix.settings.target }}
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
cd packages/desktop
|
||||||
|
bun ./scripts/prepare.ts
|
||||||
|
env:
|
||||||
|
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||||
|
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||||
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
RUST_TARGET: ${{ matrix.settings.target }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
|
|
||||||
|
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||||
|
- name: Install tauri-cli from portable appimage branch
|
||||||
|
if: contains(matrix.settings.host, 'ubuntu')
|
||||||
|
run: |
|
||||||
|
cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
|
||||||
|
echo "Installed tauri-cli version:"
|
||||||
|
cargo tauri --version
|
||||||
|
|
||||||
|
- name: Build and upload artifacts
|
||||||
|
uses: Wandalen/wretry.action@v3
|
||||||
|
timeout-minutes: 60
|
||||||
|
with:
|
||||||
|
attempt_limit: 3
|
||||||
|
attempt_delay: 10000
|
||||||
|
action: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||||
|
with: |
|
||||||
|
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
|
||||||
|
updaterJsonPreferNsis: true
|
||||||
|
releaseId: ${{ needs.publish.outputs.release }}
|
||||||
|
tagName: ${{ needs.publish.outputs.tag }}
|
||||||
|
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||||
|
releaseDraft: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_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 }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||||
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||||
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||||
|
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||||
|
|
||||||
|
publish-release:
|
||||||
|
needs:
|
||||||
|
- publish
|
||||||
|
- publish-tauri
|
||||||
|
if: needs.publish.outputs.tag
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ needs.publish.outputs.tag }}
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- name: Setup SSH for AUR
|
- name: Setup SSH for AUR
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -244,11 +230,8 @@ jobs:
|
|||||||
git config --global user.name "opencode"
|
git config --global user.name "opencode"
|
||||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||||
|
|
||||||
- run: ./script/publish.ts
|
- run: ./script/publish-complete.ts
|
||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
|
||||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
||||||
NPM_CONFIG_PROVENANCE: false
|
|
||||||
|
|||||||
@@ -29,8 +29,7 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a> |
|
<a href="README.br.md">Português (Brasil)</a>
|
||||||
<a href="README.th.md">ไทย</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
-134
@@ -1,134 +0,0 @@
|
|||||||
<p align="center">
|
|
||||||
<a href="https://opencode.ai">
|
|
||||||
<picture>
|
|
||||||
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
|
||||||
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
|
||||||
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p align="center">เอเจนต์การเขียนโค้ดด้วย AI แบบโอเพนซอร์ส</p>
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
|
||||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
|
||||||
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="สถานะการสร้าง" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="README.md">English</a> |
|
|
||||||
<a href="README.zh.md">简体中文</a> |
|
|
||||||
<a href="README.zht.md">繁體中文</a> |
|
|
||||||
<a href="README.ko.md">한국어</a> |
|
|
||||||
<a href="README.de.md">Deutsch</a> |
|
|
||||||
<a href="README.es.md">Español</a> |
|
|
||||||
<a href="README.fr.md">Français</a> |
|
|
||||||
<a href="README.it.md">Italiano</a> |
|
|
||||||
<a href="README.da.md">Dansk</a> |
|
|
||||||
<a href="README.ja.md">日本語</a> |
|
|
||||||
<a href="README.pl.md">Polski</a> |
|
|
||||||
<a href="README.ru.md">Русский</a> |
|
|
||||||
<a href="README.ar.md">العربية</a> |
|
|
||||||
<a href="README.no.md">Norsk</a> |
|
|
||||||
<a href="README.br.md">Português (Brasil)</a> |
|
|
||||||
<a href="README.th.md">ไทย</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
[](https://opencode.ai)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### การติดตั้ง
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# YOLO
|
|
||||||
curl -fsSL https://opencode.ai/install | bash
|
|
||||||
|
|
||||||
# ตัวจัดการแพ็กเกจ
|
|
||||||
npm i -g opencode-ai@latest # หรือ bun/pnpm/yarn
|
|
||||||
scoop install opencode # Windows
|
|
||||||
choco install opencode # Windows
|
|
||||||
brew install anomalyco/tap/opencode # macOS และ Linux (แนะนำ อัปเดตเสมอ)
|
|
||||||
brew install opencode # macOS และ Linux (brew formula อย่างเป็นทางการ อัปเดตน้อยกว่า)
|
|
||||||
paru -S opencode-bin # Arch Linux
|
|
||||||
mise use -g opencode # ระบบปฏิบัติการใดก็ได้
|
|
||||||
nix run nixpkgs#opencode # หรือ github:anomalyco/opencode สำหรับสาขาพัฒนาล่าสุด
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> ลบเวอร์ชันที่เก่ากว่า 0.1.x ก่อนติดตั้ง
|
|
||||||
|
|
||||||
### แอปพลิเคชันเดสก์ท็อป (เบต้า)
|
|
||||||
|
|
||||||
OpenCode มีให้ใช้งานเป็นแอปพลิเคชันเดสก์ท็อป ดาวน์โหลดโดยตรงจาก [หน้ารุ่น](https://github.com/anomalyco/opencode/releases) หรือ [opencode.ai/download](https://opencode.ai/download)
|
|
||||||
|
|
||||||
| แพลตฟอร์ม | ดาวน์โหลด |
|
|
||||||
| --------------------- | ------------------------------------- |
|
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
|
||||||
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
|
||||||
| Linux | `.deb`, `.rpm`, หรือ AppImage |
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# macOS (Homebrew)
|
|
||||||
brew install --cask opencode-desktop
|
|
||||||
# Windows (Scoop)
|
|
||||||
scoop bucket add extras; scoop install extras/opencode-desktop
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ไดเรกทอรีการติดตั้ง
|
|
||||||
|
|
||||||
สคริปต์การติดตั้งจะใช้ลำดับความสำคัญตามเส้นทางการติดตั้ง:
|
|
||||||
|
|
||||||
1. `$OPENCODE_INSTALL_DIR` - ไดเรกทอรีการติดตั้งที่กำหนดเอง
|
|
||||||
2. `$XDG_BIN_DIR` - เส้นทางที่สอดคล้องกับ XDG Base Directory Specification
|
|
||||||
3. `$HOME/bin` - ไดเรกทอรีไบนารีผู้ใช้มาตรฐาน (หากมีอยู่หรือสามารถสร้างได้)
|
|
||||||
4. `$HOME/.opencode/bin` - ค่าสำรองเริ่มต้น
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ตัวอย่าง
|
|
||||||
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
|
||||||
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
### เอเจนต์
|
|
||||||
|
|
||||||
OpenCode รวมเอเจนต์ในตัวสองตัวที่คุณสามารถสลับได้ด้วยปุ่ม `Tab`
|
|
||||||
|
|
||||||
- **build** - เอเจนต์เริ่มต้น มีสิทธิ์เข้าถึงแบบเต็มสำหรับงานพัฒนา
|
|
||||||
- **plan** - เอเจนต์อ่านอย่างเดียวสำหรับการวิเคราะห์และการสำรวจโค้ด
|
|
||||||
- ปฏิเสธการแก้ไขไฟล์โดยค่าเริ่มต้น
|
|
||||||
- ขอสิทธิ์ก่อนเรียกใช้คำสั่ง bash
|
|
||||||
- เหมาะสำหรับสำรวจโค้ดเบสที่ไม่คุ้นเคยหรือวางแผนการเปลี่ยนแปลง
|
|
||||||
|
|
||||||
นอกจากนี้ยังมีเอเจนต์ย่อย **general** สำหรับการค้นหาที่ซับซ้อนและงานหลายขั้นตอน
|
|
||||||
ใช้ภายในและสามารถเรียกใช้ได้โดยใช้ `@general` ในข้อความ
|
|
||||||
|
|
||||||
เรียนรู้เพิ่มเติมเกี่ยวกับ [เอเจนต์](https://opencode.ai/docs/agents)
|
|
||||||
|
|
||||||
### เอกสารประกอบ
|
|
||||||
|
|
||||||
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีกำหนดค่า OpenCode [**ไปที่เอกสารของเรา**](https://opencode.ai/docs)
|
|
||||||
|
|
||||||
### การมีส่วนร่วม
|
|
||||||
|
|
||||||
หากคุณสนใจที่จะมีส่วนร่วมใน OpenCode โปรดอ่าน [เอกสารการมีส่วนร่วม](./CONTRIBUTING.md) ก่อนส่ง Pull Request
|
|
||||||
|
|
||||||
### การสร้างบน OpenCode
|
|
||||||
|
|
||||||
หากคุณทำงานในโปรเจกต์ที่เกี่ยวข้องกับ OpenCode และใช้ "opencode" เป็นส่วนหนึ่งของชื่อ เช่น "opencode-dashboard" หรือ "opencode-mobile" โปรดเพิ่มหมายเหตุใน README ของคุณเพื่อชี้แจงว่าไม่ได้สร้างโดยทีม OpenCode และไม่ได้เกี่ยวข้องกับเราในทางใด
|
|
||||||
|
|
||||||
### คำถามที่พบบ่อย
|
|
||||||
|
|
||||||
#### ต่างจาก Claude Code อย่างไร?
|
|
||||||
|
|
||||||
คล้ายกับ Claude Code มากในแง่ความสามารถ นี่คือความแตกต่างหลัก:
|
|
||||||
|
|
||||||
- โอเพนซอร์ส 100%
|
|
||||||
- ไม่ผูกมัดกับผู้ให้บริการใดๆ แม้ว่าเราจะแนะนำโมเดลที่เราจัดหาให้ผ่าน [OpenCode Zen](https://opencode.ai/zen) OpenCode สามารถใช้กับ Claude, OpenAI, Google หรือแม้กระทั่งโมเดลในเครื่องได้ เมื่อโมเดลพัฒนาช่องว่างระหว่างพวกมันจะปิดลงและราคาจะลดลง ดังนั้นการไม่ผูกมัดกับผู้ให้บริการจึงสำคัญ
|
|
||||||
- รองรับ LSP ใช้งานได้ทันทีหลังการติดตั้งโดยไม่ต้องปรับแต่งหรือเปลี่ยนแปลงฟังก์ชันการทำงานใด ๆ
|
|
||||||
- เน้นที่ TUI OpenCode สร้างโดยผู้ใช้ neovim และผู้สร้าง [terminal.shop](https://terminal.shop) เราจะผลักดันขีดจำกัดของสิ่งที่เป็นไปได้ในเทอร์มินัล
|
|
||||||
- สถาปัตยกรรมไคลเอนต์/เซิร์ฟเวอร์ ตัวอย่างเช่น อาจอนุญาตให้ OpenCode ทำงานบนคอมพิวเตอร์ของคุณ ในขณะที่คุณสามารถขับเคลื่อนจากระยะไกลผ่านแอปมือถือ หมายความว่า TUI frontend เป็นหนึ่งในไคลเอนต์ที่เป็นไปได้เท่านั้น
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**ร่วมชุมชนของเรา** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
|
||||||
@@ -1,217 +1,214 @@
|
|||||||
# Download Stats
|
# Download Stats
|
||||||
|
|
||||||
| Date | GitHub Downloads | npm Downloads | Total |
|
| Date | GitHub Downloads | npm Downloads | Total |
|
||||||
| ---------- | -------------------- | -------------------- | --------------------- |
|
| ---------- | -------------------- | -------------------- | -------------------- |
|
||||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||||
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
||||||
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
|
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
|
||||||
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
|
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
|
||||||
| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) |
|
| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) |
|
||||||
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
|
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
|
||||||
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
|
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
|
||||||
| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) |
|
| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) |
|
||||||
| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) |
|
| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) |
|
||||||
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
|
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
|
||||||
| 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) |
|
| 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) |
|
||||||
| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) |
|
| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) |
|
||||||
| 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) |
|
| 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) |
|
||||||
| 2026-01-27 | 7,208,093 (+266,473) | 2,280,762 (+48,647) | 9,488,855 (+315,120) |
|
|
||||||
| 2026-01-28 | 7,489,370 (+281,277) | 2,314,849 (+34,087) | 9,804,219 (+315,364) |
|
|
||||||
| 2026-01-29 | 7,815,471 (+326,101) | 2,374,982 (+60,133) | 10,190,453 (+386,234) |
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -182,14 +182,12 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@solid-primitives/i18n": "2.2.1",
|
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-deep-link": "~2",
|
|
||||||
"@tauri-apps/plugin-dialog": "~2",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
"@tauri-apps/plugin-http": "~2",
|
"@tauri-apps/plugin-http": "~2",
|
||||||
"@tauri-apps/plugin-notification": "~2",
|
"@tauri-apps/plugin-notification": "~2",
|
||||||
@@ -213,7 +211,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -242,7 +240,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -258,7 +256,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -362,7 +360,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -382,7 +380,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.90.10",
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -393,7 +391,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -406,7 +404,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -448,7 +446,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -459,7 +457,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1749,8 +1747,6 @@
|
|||||||
|
|
||||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="],
|
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.6", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA=="],
|
|
||||||
|
|
||||||
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="],
|
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="],
|
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-yAtZlh6YR78RwPt0LK/7Pk0qUm0/97+s6ghhZzuoE/0=",
|
"x86_64-linux": "sha256-AkI3guNjnE+bLZQVfzm0z14UENOECv2QBqMo5Lzkvt8=",
|
||||||
"aarch64-linux": "sha256-6j81rdjQ7Wps9bvfw+mmdwW5p01qUOwX40UZltCTe3Y=",
|
"aarch64-linux": "sha256-dBfdyVTqW+fBZKCxC9Ld+1m3cP+nIbS6UDo0tUfPOSk=",
|
||||||
"aarch64-darwin": "sha256-pDM8M/QMWR6Go5pz3XXsJqcJDHAlHrx2Faijjkzcngo=",
|
"aarch64-darwin": "sha256-tOw31AMnHkW2cEDi+iqT3P93lU3SiMve26TEIqPz97k=",
|
||||||
"x86_64-darwin": "sha256-eOAPtMd1n5xYupBOevCLhY1eFy3wzGqFk/EsZocl9Y8="
|
"x86_64-darwin": "sha256-wL/DmdZmxCmh+r4dsS1XGXuj8VPwR4pUqy5VIA76jl0="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { test, expect } from "./fixtures"
|
|||||||
test("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
|
test("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
|
||||||
const toggle = page.getByRole("button", { name: "Toggle file tree" })
|
await page.getByRole("button", { name: "Toggle file tree" }).click()
|
||||||
const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
|
|
||||||
|
|
||||||
if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click()
|
const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
|
||||||
await expect(treeTabs).toBeVisible()
|
await expect(treeTabs).toBeVisible()
|
||||||
|
|
||||||
await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click()
|
await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click()
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { test, expect } from "./fixtures"
|
|
||||||
import { modKey, promptSelector } from "./utils"
|
|
||||||
|
|
||||||
test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
|
|
||||||
await gotoSession()
|
|
||||||
|
|
||||||
await page.locator(promptSelector).click()
|
|
||||||
await page.keyboard.type("/model")
|
|
||||||
|
|
||||||
const command = page.locator('[data-slash-id="model.choose"]')
|
|
||||||
await expect(command).toBeVisible()
|
|
||||||
await command.hover()
|
|
||||||
await page.keyboard.press("Enter")
|
|
||||||
|
|
||||||
const picker = page.getByRole("dialog")
|
|
||||||
await expect(picker).toBeVisible()
|
|
||||||
|
|
||||||
const target = picker.locator('[data-slot="list-item"]').first()
|
|
||||||
await expect(target).toBeVisible()
|
|
||||||
|
|
||||||
const key = await target.getAttribute("data-key")
|
|
||||||
if (!key) throw new Error("Failed to resolve model key from list item")
|
|
||||||
|
|
||||||
const name = (await target.locator("span").first().innerText()).trim()
|
|
||||||
if (!name) throw new Error("Failed to resolve model name from list item")
|
|
||||||
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
await expect(picker).toHaveCount(0)
|
|
||||||
|
|
||||||
const settings = page.getByRole("dialog")
|
|
||||||
|
|
||||||
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
|
|
||||||
const opened = await settings
|
|
||||||
.waitFor({ state: "visible", timeout: 3000 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
|
|
||||||
if (!opened) {
|
|
||||||
await page.getByRole("button", { name: "Settings" }).first().click()
|
|
||||||
await expect(settings).toBeVisible()
|
|
||||||
}
|
|
||||||
|
|
||||||
await settings.getByRole("tab", { name: "Models" }).click()
|
|
||||||
const search = settings.getByPlaceholder("Search models")
|
|
||||||
await expect(search).toBeVisible()
|
|
||||||
await search.fill(name)
|
|
||||||
|
|
||||||
const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
|
|
||||||
const input = toggle.locator('[data-slot="switch-input"]')
|
|
||||||
await expect(toggle).toBeVisible()
|
|
||||||
await expect(input).toHaveAttribute("aria-checked", "true")
|
|
||||||
await toggle.locator('[data-slot="switch-control"]').click()
|
|
||||||
await expect(input).toHaveAttribute("aria-checked", "false")
|
|
||||||
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
const closed = await settings
|
|
||||||
.waitFor({ state: "detached", timeout: 1500 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
if (!closed) {
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
const closedSecond = await settings
|
|
||||||
.waitFor({ state: "detached", timeout: 1500 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
if (!closedSecond) {
|
|
||||||
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
|
|
||||||
await expect(settings).toHaveCount(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.locator(promptSelector).click()
|
|
||||||
await page.keyboard.type("/model")
|
|
||||||
await expect(command).toBeVisible()
|
|
||||||
await command.hover()
|
|
||||||
await page.keyboard.press("Enter")
|
|
||||||
|
|
||||||
const pickerAgain = page.getByRole("dialog")
|
|
||||||
await expect(pickerAgain).toBeVisible()
|
|
||||||
await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible()
|
|
||||||
|
|
||||||
await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0)
|
|
||||||
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
await expect(pickerAgain).toHaveCount(0)
|
|
||||||
})
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { test, expect } from "./fixtures"
|
|
||||||
import { serverName, serverUrl } from "./utils"
|
|
||||||
|
|
||||||
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
|
|
||||||
|
|
||||||
test("can set a default server on web", async ({ page, gotoSession }) => {
|
|
||||||
await page.addInitScript((key: string) => {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem(key)
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}, DEFAULT_SERVER_URL_KEY)
|
|
||||||
|
|
||||||
await gotoSession()
|
|
||||||
|
|
||||||
const status = page.getByRole("button", { name: "Status" })
|
|
||||||
await expect(status).toBeVisible()
|
|
||||||
const popover = page.locator('[data-component="popover-content"]').filter({ hasText: "Manage servers" })
|
|
||||||
|
|
||||||
const ensurePopoverOpen = async () => {
|
|
||||||
if (await popover.isVisible()) return
|
|
||||||
await status.click()
|
|
||||||
await expect(popover).toBeVisible()
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensurePopoverOpen()
|
|
||||||
await popover.getByRole("button", { name: "Manage servers" }).click()
|
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog")
|
|
||||||
await expect(dialog).toBeVisible()
|
|
||||||
|
|
||||||
const row = dialog.locator('[data-slot="list-item"]').filter({ hasText: serverName }).first()
|
|
||||||
await expect(row).toBeVisible()
|
|
||||||
|
|
||||||
const menu = row.locator('[data-component="icon-button"]').last()
|
|
||||||
await menu.click()
|
|
||||||
await page.getByRole("menuitem", { name: "Set as default" }).click()
|
|
||||||
|
|
||||||
await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)).toBe(serverUrl)
|
|
||||||
await expect(row.getByText("Default", { exact: true })).toBeVisible()
|
|
||||||
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
const closed = await dialog
|
|
||||||
.waitFor({ state: "detached", timeout: 1500 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
|
|
||||||
if (!closed) {
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
const closedSecond = await dialog
|
|
||||||
.waitFor({ state: "detached", timeout: 1500 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
|
|
||||||
if (!closedSecond) {
|
|
||||||
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
|
|
||||||
await expect(dialog).toHaveCount(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensurePopoverOpen()
|
|
||||||
|
|
||||||
const serverRow = popover.locator("button").filter({ hasText: serverName }).first()
|
|
||||||
await expect(serverRow).toBeVisible()
|
|
||||||
await expect(serverRow.getByText("Default", { exact: true })).toBeVisible()
|
|
||||||
})
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { test, expect } from "./fixtures"
|
|
||||||
import { modKey, promptSelector } from "./utils"
|
|
||||||
|
|
||||||
test("smoke providers settings opens provider selector", async ({ page, gotoSession }) => {
|
|
||||||
await gotoSession()
|
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog")
|
|
||||||
|
|
||||||
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
|
|
||||||
|
|
||||||
const opened = await dialog
|
|
||||||
.waitFor({ state: "visible", timeout: 3000 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
|
|
||||||
if (!opened) {
|
|
||||||
await page.getByRole("button", { name: "Settings" }).first().click()
|
|
||||||
await expect(dialog).toBeVisible()
|
|
||||||
}
|
|
||||||
|
|
||||||
await dialog.getByRole("tab", { name: "Providers" }).click()
|
|
||||||
await expect(dialog.getByText("Connected providers", { exact: true })).toBeVisible()
|
|
||||||
await expect(dialog.getByText("Popular providers", { exact: true })).toBeVisible()
|
|
||||||
|
|
||||||
await dialog.getByRole("button", { name: "Show more providers" }).click()
|
|
||||||
|
|
||||||
const providerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder("Search providers") })
|
|
||||||
|
|
||||||
await expect(providerDialog).toBeVisible()
|
|
||||||
await expect(providerDialog.getByPlaceholder("Search providers")).toBeVisible()
|
|
||||||
await expect(providerDialog.locator('[data-slot="list-item"]').first()).toBeVisible()
|
|
||||||
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
await expect(providerDialog).toHaveCount(0)
|
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
|
|
||||||
const stillOpen = await dialog.isVisible().catch(() => false)
|
|
||||||
if (!stillOpen) return
|
|
||||||
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
const closed = await dialog
|
|
||||||
.waitFor({ state: "detached", timeout: 1500 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
if (closed) return
|
|
||||||
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
const closedSecond = await dialog
|
|
||||||
.waitFor({ state: "detached", timeout: 1500 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
if (closedSecond) return
|
|
||||||
|
|
||||||
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
|
|
||||||
await expect(dialog).toHaveCount(0)
|
|
||||||
})
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { test, expect } from "./fixtures"
|
|
||||||
import { modKey, promptSelector } from "./utils"
|
|
||||||
|
|
||||||
test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => {
|
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
|
||||||
|
|
||||||
const stamp = Date.now()
|
|
||||||
const one = await sdk.session.create({ title: `e2e titlebar history 1 ${stamp}` }).then((r) => r.data)
|
|
||||||
const two = await sdk.session.create({ title: `e2e titlebar history 2 ${stamp}` }).then((r) => r.data)
|
|
||||||
|
|
||||||
if (!one?.id) throw new Error("Session create did not return an id")
|
|
||||||
if (!two?.id) throw new Error("Session create did not return an id")
|
|
||||||
|
|
||||||
try {
|
|
||||||
await gotoSession(one.id)
|
|
||||||
|
|
||||||
const main = page.locator("main")
|
|
||||||
const collapsed = ((await main.getAttribute("class")) ?? "").includes("xl:border-l")
|
|
||||||
if (collapsed) {
|
|
||||||
await page.keyboard.press(`${modKey}+B`)
|
|
||||||
await expect(main).not.toHaveClass(/xl:border-l/)
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = page.locator(`[data-session-id="${two.id}"] a`).first()
|
|
||||||
await expect(link).toBeVisible()
|
|
||||||
await link.scrollIntoViewIfNeeded()
|
|
||||||
await link.click()
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
|
|
||||||
const back = page.getByRole("button", { name: "Back" })
|
|
||||||
const forward = page.getByRole("button", { name: "Forward" })
|
|
||||||
|
|
||||||
await expect(back).toBeVisible()
|
|
||||||
await expect(back).toBeEnabled()
|
|
||||||
await back.click()
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
|
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
|
|
||||||
await expect(forward).toBeVisible()
|
|
||||||
await expect(forward).toBeEnabled()
|
|
||||||
await forward.click()
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
} finally {
|
|
||||||
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
|
|
||||||
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function UiI18nBridge(props: ParentProps) {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
|
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,17 +27,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const alive = { value: true }
|
|
||||||
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
alive.value = false
|
|
||||||
if (timer.current === undefined) return
|
|
||||||
clearTimeout(timer.current)
|
|
||||||
timer.current = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
|
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
|
||||||
const methods = createMemo(
|
const methods = createMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -64,11 +53,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function selectMethod(index: number) {
|
async function selectMethod(index: number) {
|
||||||
if (timer.current !== undefined) {
|
|
||||||
clearTimeout(timer.current)
|
|
||||||
timer.current = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const method = methods()[index]
|
const method = methods()[index]
|
||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
@@ -91,15 +75,11 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
{ throwOnError: true },
|
{ throwOnError: true },
|
||||||
)
|
)
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
if (!alive.value) return
|
|
||||||
const elapsed = Date.now() - start
|
const elapsed = Date.now() - start
|
||||||
const delay = 1000 - elapsed
|
const delay = 1000 - elapsed
|
||||||
|
|
||||||
if (delay > 0) {
|
if (delay > 0) {
|
||||||
if (timer.current !== undefined) clearTimeout(timer.current)
|
setTimeout(() => {
|
||||||
timer.current = setTimeout(() => {
|
|
||||||
timer.current = undefined
|
|
||||||
if (!alive.value) return
|
|
||||||
setStore("state", "complete")
|
setStore("state", "complete")
|
||||||
setStore("authorization", x.data!)
|
setStore("authorization", x.data!)
|
||||||
}, delay)
|
}, delay)
|
||||||
@@ -109,7 +89,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
setStore("authorization", x.data!)
|
setStore("authorization", x.data!)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (!alive.value) return
|
|
||||||
setStore("state", "error")
|
setStore("state", "error")
|
||||||
setStore("error", String(e))
|
setStore("error", String(e))
|
||||||
})
|
})
|
||||||
@@ -393,33 +372,26 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
return instructions
|
return instructions
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
void (async () => {
|
if (store.authorization?.url) {
|
||||||
if (store.authorization?.url) {
|
platform.openLink(store.authorization.url)
|
||||||
platform.openLink(store.authorization.url)
|
}
|
||||||
}
|
const result = await globalSDK.client.provider.oauth
|
||||||
|
.callback({
|
||||||
const result = await globalSDK.client.provider.oauth
|
providerID: props.provider,
|
||||||
.callback({
|
method: store.methodIndex,
|
||||||
providerID: props.provider,
|
})
|
||||||
method: store.methodIndex,
|
.then((value) =>
|
||||||
})
|
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
|
||||||
.then((value) =>
|
)
|
||||||
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
|
.catch((error) => ({ ok: false as const, error }))
|
||||||
)
|
if (!result.ok) {
|
||||||
.catch((error) => ({ ok: false as const, error }))
|
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
||||||
|
setStore("state", "error")
|
||||||
if (!alive.value) return
|
setStore("error", message)
|
||||||
|
return
|
||||||
if (!result.ok) {
|
}
|
||||||
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
await complete()
|
||||||
setStore("state", "error")
|
|
||||||
setStore("error", message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await complete()
|
|
||||||
})()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,424 +0,0 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { For } from "solid-js"
|
|
||||||
import { createStore, produce } from "solid-js/store"
|
|
||||||
import { Link } from "@/components/link"
|
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
|
||||||
|
|
||||||
const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
|
|
||||||
const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
back?: "providers" | "close"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DialogCustomProvider(props: Props) {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const globalSync = useGlobalSync()
|
|
||||||
const globalSDK = useGlobalSDK()
|
|
||||||
const language = useLanguage()
|
|
||||||
|
|
||||||
const [form, setForm] = createStore({
|
|
||||||
providerID: "",
|
|
||||||
name: "",
|
|
||||||
baseURL: "",
|
|
||||||
apiKey: "",
|
|
||||||
models: [{ id: "", name: "" }],
|
|
||||||
headers: [{ key: "", value: "" }],
|
|
||||||
saving: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [errors, setErrors] = createStore({
|
|
||||||
providerID: undefined as string | undefined,
|
|
||||||
name: undefined as string | undefined,
|
|
||||||
baseURL: undefined as string | undefined,
|
|
||||||
models: [{} as { id?: string; name?: string }],
|
|
||||||
headers: [{} as { key?: string; value?: string }],
|
|
||||||
})
|
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
if (props.back === "close") {
|
|
||||||
dialog.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addModel = () => {
|
|
||||||
setForm(
|
|
||||||
"models",
|
|
||||||
produce((draft) => {
|
|
||||||
draft.push({ id: "", name: "" })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
setErrors(
|
|
||||||
"models",
|
|
||||||
produce((draft) => {
|
|
||||||
draft.push({})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeModel = (index: number) => {
|
|
||||||
if (form.models.length <= 1) return
|
|
||||||
setForm(
|
|
||||||
"models",
|
|
||||||
produce((draft) => {
|
|
||||||
draft.splice(index, 1)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
setErrors(
|
|
||||||
"models",
|
|
||||||
produce((draft) => {
|
|
||||||
draft.splice(index, 1)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addHeader = () => {
|
|
||||||
setForm(
|
|
||||||
"headers",
|
|
||||||
produce((draft) => {
|
|
||||||
draft.push({ key: "", value: "" })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
setErrors(
|
|
||||||
"headers",
|
|
||||||
produce((draft) => {
|
|
||||||
draft.push({})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeHeader = (index: number) => {
|
|
||||||
if (form.headers.length <= 1) return
|
|
||||||
setForm(
|
|
||||||
"headers",
|
|
||||||
produce((draft) => {
|
|
||||||
draft.splice(index, 1)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
setErrors(
|
|
||||||
"headers",
|
|
||||||
produce((draft) => {
|
|
||||||
draft.splice(index, 1)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const validate = () => {
|
|
||||||
const providerID = form.providerID.trim()
|
|
||||||
const name = form.name.trim()
|
|
||||||
const baseURL = form.baseURL.trim()
|
|
||||||
const apiKey = form.apiKey.trim()
|
|
||||||
|
|
||||||
const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
|
|
||||||
const key = apiKey && !env ? apiKey : undefined
|
|
||||||
|
|
||||||
const idError = !providerID
|
|
||||||
? "Provider ID is required"
|
|
||||||
: !PROVIDER_ID.test(providerID)
|
|
||||||
? "Use lowercase letters, numbers, hyphens, or underscores"
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const nameError = !name ? "Display name is required" : undefined
|
|
||||||
const urlError = !baseURL
|
|
||||||
? "Base URL is required"
|
|
||||||
: !/^https?:\/\//.test(baseURL)
|
|
||||||
? "Must start with http:// or https://"
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const disabled = (globalSync.data.config.disabled_providers ?? []).includes(providerID)
|
|
||||||
const existingProvider = globalSync.data.provider.all.find((p) => p.id === providerID)
|
|
||||||
const existsError = idError
|
|
||||||
? undefined
|
|
||||||
: existingProvider && !disabled
|
|
||||||
? "That provider ID already exists"
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const seenModels = new Set<string>()
|
|
||||||
const modelErrors = form.models.map((m) => {
|
|
||||||
const id = m.id.trim()
|
|
||||||
const modelIdError = !id
|
|
||||||
? "Required"
|
|
||||||
: seenModels.has(id)
|
|
||||||
? "Duplicate"
|
|
||||||
: (() => {
|
|
||||||
seenModels.add(id)
|
|
||||||
return undefined
|
|
||||||
})()
|
|
||||||
const modelNameError = !m.name.trim() ? "Required" : undefined
|
|
||||||
return { id: modelIdError, name: modelNameError }
|
|
||||||
})
|
|
||||||
const modelsValid = modelErrors.every((m) => !m.id && !m.name)
|
|
||||||
const models = Object.fromEntries(form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
|
|
||||||
|
|
||||||
const seenHeaders = new Set<string>()
|
|
||||||
const headerErrors = form.headers.map((h) => {
|
|
||||||
const key = h.key.trim()
|
|
||||||
const value = h.value.trim()
|
|
||||||
|
|
||||||
if (!key && !value) return {}
|
|
||||||
const keyError = !key
|
|
||||||
? "Required"
|
|
||||||
: seenHeaders.has(key.toLowerCase())
|
|
||||||
? "Duplicate"
|
|
||||||
: (() => {
|
|
||||||
seenHeaders.add(key.toLowerCase())
|
|
||||||
return undefined
|
|
||||||
})()
|
|
||||||
const valueError = !value ? "Required" : undefined
|
|
||||||
return { key: keyError, value: valueError }
|
|
||||||
})
|
|
||||||
const headersValid = headerErrors.every((h) => !h.key && !h.value)
|
|
||||||
const headers = Object.fromEntries(
|
|
||||||
form.headers
|
|
||||||
.map((h) => ({ key: h.key.trim(), value: h.value.trim() }))
|
|
||||||
.filter((h) => !!h.key && !!h.value)
|
|
||||||
.map((h) => [h.key, h.value]),
|
|
||||||
)
|
|
||||||
|
|
||||||
setErrors(
|
|
||||||
produce((draft) => {
|
|
||||||
draft.providerID = idError ?? existsError
|
|
||||||
draft.name = nameError
|
|
||||||
draft.baseURL = urlError
|
|
||||||
draft.models = modelErrors
|
|
||||||
draft.headers = headerErrors
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
|
|
||||||
if (!ok) return
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
baseURL,
|
|
||||||
...(Object.keys(headers).length ? { headers } : {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
providerID,
|
|
||||||
name,
|
|
||||||
key,
|
|
||||||
config: {
|
|
||||||
npm: OPENAI_COMPATIBLE,
|
|
||||||
name,
|
|
||||||
...(env ? { env: [env] } : {}),
|
|
||||||
options,
|
|
||||||
models,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async (e: SubmitEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (form.saving) return
|
|
||||||
|
|
||||||
const result = validate()
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
setForm("saving", true)
|
|
||||||
|
|
||||||
const disabledProviders = globalSync.data.config.disabled_providers ?? []
|
|
||||||
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
|
|
||||||
|
|
||||||
const auth = result.key
|
|
||||||
? globalSDK.client.auth.set({
|
|
||||||
providerID: result.providerID,
|
|
||||||
auth: {
|
|
||||||
type: "api",
|
|
||||||
key: result.key,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: Promise.resolve()
|
|
||||||
|
|
||||||
auth
|
|
||||||
.then(() =>
|
|
||||||
globalSync.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled }),
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
dialog.close()
|
|
||||||
showToast({
|
|
||||||
variant: "success",
|
|
||||||
icon: "circle-check",
|
|
||||||
title: language.t("provider.connect.toast.connected.title", { provider: result.name }),
|
|
||||||
description: language.t("provider.connect.toast.connected.description", { provider: result.name }),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setForm("saving", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
title={
|
|
||||||
<IconButton
|
|
||||||
tabIndex={-1}
|
|
||||||
icon="arrow-left"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={goBack}
|
|
||||||
aria-label={language.t("common.goBack")}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
transition
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
|
|
||||||
<div class="px-2.5 flex gap-4 items-center">
|
|
||||||
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
|
||||||
<div class="text-16-medium text-text-strong">Custom provider</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
|
|
||||||
<p class="text-14-regular text-text-base">
|
|
||||||
Configure an OpenAI-compatible provider. See the{" "}
|
|
||||||
<Link href="https://opencode.ai/docs/providers/#custom-provider" tabIndex={-1}>
|
|
||||||
provider config docs
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<TextField
|
|
||||||
autofocus
|
|
||||||
label="Provider ID"
|
|
||||||
placeholder="myprovider"
|
|
||||||
description="Lowercase letters, numbers, hyphens, or underscores"
|
|
||||||
value={form.providerID}
|
|
||||||
onChange={setForm.bind(null, "providerID")}
|
|
||||||
validationState={errors.providerID ? "invalid" : undefined}
|
|
||||||
error={errors.providerID}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Display name"
|
|
||||||
placeholder="My AI Provider"
|
|
||||||
value={form.name}
|
|
||||||
onChange={setForm.bind(null, "name")}
|
|
||||||
validationState={errors.name ? "invalid" : undefined}
|
|
||||||
error={errors.name}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Base URL"
|
|
||||||
placeholder="https://api.myprovider.com/v1"
|
|
||||||
value={form.baseURL}
|
|
||||||
onChange={setForm.bind(null, "baseURL")}
|
|
||||||
validationState={errors.baseURL ? "invalid" : undefined}
|
|
||||||
error={errors.baseURL}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="API key"
|
|
||||||
placeholder="API key"
|
|
||||||
description="Optional. Leave empty if you manage auth via headers."
|
|
||||||
value={form.apiKey}
|
|
||||||
onChange={setForm.bind(null, "apiKey")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<label class="text-12-medium text-text-weak">Models</label>
|
|
||||||
<For each={form.models}>
|
|
||||||
{(m, i) => (
|
|
||||||
<div class="flex gap-2 items-start">
|
|
||||||
<div class="flex-1">
|
|
||||||
<TextField
|
|
||||||
label="ID"
|
|
||||||
hideLabel
|
|
||||||
placeholder="model-id"
|
|
||||||
value={m.id}
|
|
||||||
onChange={(v) => setForm("models", i(), "id", v)}
|
|
||||||
validationState={errors.models[i()]?.id ? "invalid" : undefined}
|
|
||||||
error={errors.models[i()]?.id}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<TextField
|
|
||||||
label="Name"
|
|
||||||
hideLabel
|
|
||||||
placeholder="Display Name"
|
|
||||||
value={m.name}
|
|
||||||
onChange={(v) => setForm("models", i(), "name", v)}
|
|
||||||
validationState={errors.models[i()]?.name ? "invalid" : undefined}
|
|
||||||
error={errors.models[i()]?.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<IconButton
|
|
||||||
type="button"
|
|
||||||
icon="trash"
|
|
||||||
variant="ghost"
|
|
||||||
class="mt-1.5"
|
|
||||||
onClick={() => removeModel(i())}
|
|
||||||
disabled={form.models.length <= 1}
|
|
||||||
aria-label="Remove model"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addModel} class="self-start">
|
|
||||||
Add model
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<label class="text-12-medium text-text-weak">Headers (optional)</label>
|
|
||||||
<For each={form.headers}>
|
|
||||||
{(h, i) => (
|
|
||||||
<div class="flex gap-2 items-start">
|
|
||||||
<div class="flex-1">
|
|
||||||
<TextField
|
|
||||||
label="Header"
|
|
||||||
hideLabel
|
|
||||||
placeholder="Header-Name"
|
|
||||||
value={h.key}
|
|
||||||
onChange={(v) => setForm("headers", i(), "key", v)}
|
|
||||||
validationState={errors.headers[i()]?.key ? "invalid" : undefined}
|
|
||||||
error={errors.headers[i()]?.key}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<TextField
|
|
||||||
label="Value"
|
|
||||||
hideLabel
|
|
||||||
placeholder="value"
|
|
||||||
value={h.value}
|
|
||||||
onChange={(v) => setForm("headers", i(), "value", v)}
|
|
||||||
validationState={errors.headers[i()]?.value ? "invalid" : undefined}
|
|
||||||
error={errors.headers[i()]?.value}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<IconButton
|
|
||||||
type="button"
|
|
||||||
icon="trash"
|
|
||||||
variant="ghost"
|
|
||||||
class="mt-1.5"
|
|
||||||
onClick={() => removeHeader(i())}
|
|
||||||
disabled={form.headers.length <= 1}
|
|
||||||
aria-label="Remove header"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addHeader} class="self-start">
|
|
||||||
Add header
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button class="w-auto self-start" type="submit" size="large" variant="primary" disabled={form.saving}>
|
|
||||||
{form.saving ? "Saving..." : language.t("common.submit")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -145,7 +145,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
<Avatar
|
<Avatar
|
||||||
fallback={store.name || defaultName()}
|
fallback={store.name || defaultName()}
|
||||||
{...getAvatarColors(store.color)}
|
{...getAvatarColors(store.color)}
|
||||||
class="size-full text-[32px]"
|
class="size-full"
|
||||||
|
style={{ "font-size": "32px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -158,19 +159,39 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
style={{
|
||||||
classList={{
|
position: "absolute",
|
||||||
"opacity-100": store.iconHover && !store.iconUrl,
|
top: 0,
|
||||||
"opacity-0": !(store.iconHover && !store.iconUrl),
|
left: 0,
|
||||||
|
width: "64px",
|
||||||
|
height: "64px",
|
||||||
|
background: "rgba(0,0,0,0.6)",
|
||||||
|
"border-radius": "6px",
|
||||||
|
"z-index": 10,
|
||||||
|
"pointer-events": "none",
|
||||||
|
opacity: store.iconHover && !store.iconUrl ? 1 : 0,
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="cloud-upload" size="large" class="text-icon-invert-base" />
|
<Icon name="cloud-upload" size="large" class="text-icon-invert-base" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
style={{
|
||||||
classList={{
|
position: "absolute",
|
||||||
"opacity-100": store.iconHover && !!store.iconUrl,
|
top: 0,
|
||||||
"opacity-0": !(store.iconHover && !!store.iconUrl),
|
left: 0,
|
||||||
|
width: "64px",
|
||||||
|
height: "64px",
|
||||||
|
background: "rgba(0,0,0,0.6)",
|
||||||
|
"border-radius": "6px",
|
||||||
|
"z-index": 10,
|
||||||
|
"pointer-events": "none",
|
||||||
|
opacity: store.iconHover && store.iconUrl ? 1 : 0,
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="trash" size="large" class="text-icon-invert-base" />
|
<Icon name="trash" size="large" class="text-icon-invert-base" />
|
||||||
|
|||||||
@@ -90,8 +90,12 @@ export const DialogFork: Component = () => {
|
|||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div class="w-full flex items-center gap-2">
|
<div class="w-full flex items-center gap-2">
|
||||||
<span class="truncate flex-1 min-w-0 text-left font-normal">{item.text}</span>
|
<span class="truncate flex-1 min-w-0 text-left" style={{ "font-weight": "400" }}>
|
||||||
<span class="text-text-weak shrink-0 font-normal">{item.time}</span>
|
{item.text}
|
||||||
|
</span>
|
||||||
|
<span class="text-text-weak shrink-0" style={{ "font-weight": "400" }}>
|
||||||
|
{item.time}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -1,33 +1,16 @@
|
|||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
|
||||||
import type { Component } from "solid-js"
|
import type { Component } from "solid-js"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
|
||||||
|
|
||||||
export const DialogManageModels: Component = () => {
|
export const DialogManageModels: Component = () => {
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const dialog = useDialog()
|
|
||||||
|
|
||||||
const handleConnectProvider = () => {
|
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog title={language.t("dialog.model.manage")} description={language.t("dialog.model.manage.description")}>
|
||||||
title={language.t("dialog.model.manage")}
|
|
||||||
description={language.t("dialog.model.manage.description")}
|
|
||||||
action={
|
|
||||||
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={handleConnectProvider}>
|
|
||||||
{language.t("command.provider.connect")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<List
|
<List
|
||||||
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
|
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
|
||||||
emptyMessage={language.t("dialog.model.empty")}
|
emptyMessage={language.t("dialog.model.empty")}
|
||||||
|
|||||||
@@ -73,86 +73,80 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog class="dialog-release-notes">
|
||||||
size="large"
|
|
||||||
fit
|
|
||||||
class="w-[min(calc(100vw-40px),720px)] h-[min(calc(100vh-40px),400px)] -mt-20 min-h-0 overflow-hidden"
|
|
||||||
>
|
|
||||||
{/* Hidden element to capture initial focus and handle escape */}
|
{/* Hidden element to capture initial focus and handle escape */}
|
||||||
<div ref={focusTrap} tabindex="0" class="absolute opacity-0 pointer-events-none" />
|
<div ref={focusTrap} tabindex="0" class="absolute opacity-0 pointer-events-none" />
|
||||||
<div class="flex flex-1 min-w-0 min-h-0">
|
{/* Left side - Text content */}
|
||||||
{/* Left side - Text content */}
|
<div class="flex flex-col flex-1 min-w-0 p-8">
|
||||||
<div class="flex flex-col flex-1 min-w-0 p-8">
|
{/* Top section - feature content (fixed position from top) */}
|
||||||
{/* Top section - feature content (fixed position from top) */}
|
<div class="flex flex-col gap-2 pt-22">
|
||||||
<div class="flex flex-col gap-2 pt-22">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex items-center gap-2">
|
<h1 class="text-16-medium text-text-strong">{feature()?.title ?? ""}</h1>
|
||||||
<h1 class="text-16-medium text-text-strong">{feature()?.title ?? ""}</h1>
|
|
||||||
</div>
|
|
||||||
<p class="text-14-regular text-text-base">{feature()?.description ?? ""}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Spacer to push buttons to bottom */}
|
|
||||||
<div class="flex-1" />
|
|
||||||
|
|
||||||
{/* Bottom section - buttons and indicators (fixed position) */}
|
|
||||||
<div class="flex flex-col gap-12">
|
|
||||||
<div class="flex flex-col items-start gap-3">
|
|
||||||
{isLast() ? (
|
|
||||||
<Button variant="primary" size="large" onClick={handleClose}>
|
|
||||||
Get started
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button variant="secondary" size="large" onClick={handleNext}>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="ghost" size="small" onClick={handleDisable}>
|
|
||||||
Don't show these in the future
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{paged() && (
|
|
||||||
<div class="flex items-center gap-1.5 -my-2.5">
|
|
||||||
{props.highlights.map((_, i) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
|
|
||||||
classList={{
|
|
||||||
"w-8": i === index(),
|
|
||||||
"w-3": i !== index(),
|
|
||||||
}}
|
|
||||||
onClick={() => setIndex(i)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-full h-0.5 rounded-[1px] transition-colors duration-200"
|
|
||||||
classList={{
|
|
||||||
"bg-icon-strong-base": i === index(),
|
|
||||||
"bg-icon-weak-base": i !== index(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-14-regular text-text-base">{feature()?.description ?? ""}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Media content (edge to edge) */}
|
{/* Spacer to push buttons to bottom */}
|
||||||
{feature()?.media && (
|
<div class="flex-1" />
|
||||||
<div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
|
|
||||||
{feature()!.media!.type === "image" ? (
|
{/* Bottom section - buttons and indicators (fixed position) */}
|
||||||
<img
|
<div class="flex flex-col gap-12">
|
||||||
src={feature()!.media!.src}
|
<div class="flex flex-col items-start gap-3">
|
||||||
alt={feature()!.media!.alt ?? feature()?.title ?? "Release preview"}
|
{isLast() ? (
|
||||||
class="w-full h-full object-cover"
|
<Button variant="primary" size="large" onClick={handleClose}>
|
||||||
/>
|
Get started
|
||||||
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<video src={feature()!.media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
|
<Button variant="secondary" size="large" onClick={handleNext}>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Button variant="ghost" size="small" onClick={handleDisable}>
|
||||||
|
Don't show these in the future
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{paged() && (
|
||||||
|
<div class="flex items-center gap-1.5 -my-2.5">
|
||||||
|
{props.highlights.map((_, i) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
|
||||||
|
classList={{
|
||||||
|
"w-8": i === index(),
|
||||||
|
"w-3": i !== index(),
|
||||||
|
}}
|
||||||
|
onClick={() => setIndex(i)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full h-0.5 rounded-[1px] transition-colors duration-200"
|
||||||
|
classList={{
|
||||||
|
"bg-icon-strong-base": i === index(),
|
||||||
|
"bg-icon-weak-base": i !== index(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Right side - Media content (edge to edge) */}
|
||||||
|
{feature()?.media && (
|
||||||
|
<div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
|
||||||
|
{feature()!.media!.type === "image" ? (
|
||||||
|
<img
|
||||||
|
src={feature()!.media!.src}
|
||||||
|
alt={feature()!.media!.alt ?? feature()?.title ?? "Release preview"}
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<video src={feature()!.media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type Entry = {
|
|||||||
|
|
||||||
type DialogSelectFileMode = "all" | "files"
|
type DialogSelectFileMode = "all" | "files"
|
||||||
|
|
||||||
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
export function DialogSelectFile(props: { mode?: DialogSelectFileMode }) {
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -36,6 +36,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
const filesOnly = () => props.mode === "files"
|
const filesOnly = () => props.mode === "files"
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||||
const [grouped, setGrouped] = createSignal(false)
|
const [grouped, setGrouped] = createSignal(false)
|
||||||
const common = [
|
const common = [
|
||||||
@@ -162,9 +163,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
const value = file.tab(path)
|
const value = file.tab(path)
|
||||||
tabs().open(value)
|
tabs().open(value)
|
||||||
file.load(path)
|
file.load(path)
|
||||||
layout.fileTree.open()
|
view().reviewPanel.open()
|
||||||
layout.fileTree.setTab("all")
|
|
||||||
props.onOpenFile?.(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (item: Entry | undefined) => {
|
const handleSelect = (item: Entry | undefined) => {
|
||||||
@@ -196,6 +195,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
: language.t("palette.search.placeholder"),
|
: language.t("palette.search.placeholder"),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
hideIcon: true,
|
hideIcon: true,
|
||||||
|
class: "pl-3 pr-2 !mb-0",
|
||||||
}}
|
}}
|
||||||
emptyMessage={language.t("palette.empty")}
|
emptyMessage={language.t("palette.empty")}
|
||||||
loadingMessage={language.t("common.loading")}
|
loadingMessage={language.t("common.loading")}
|
||||||
@@ -223,7 +223,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="w-full flex items-center justify-between gap-4">
|
<div class="w-full flex items-center justify-between gap-4 pl-1">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
||||||
<Show when={item.description}>
|
<Show when={item.description}>
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ const ModelList: Component<{
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
gutter={12}
|
gutter={12}
|
||||||
forceMount={false}
|
|
||||||
value={
|
value={
|
||||||
<ModelTooltip
|
<ModelTooltip
|
||||||
model={item}
|
model={item}
|
||||||
@@ -187,7 +186,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
<Kobalte.Portal>
|
<Kobalte.Portal>
|
||||||
<Kobalte.Content
|
<Kobalte.Content
|
||||||
ref={(el) => setStore("content", el)}
|
ref={(el) => setStore("content", el)}
|
||||||
class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
|
class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
|
||||||
onEscapeKeyDown={(event) => {
|
onEscapeKeyDown={(event) => {
|
||||||
setStore("dismiss", "escape")
|
setStore("dismiss", "escape")
|
||||||
setStore("open", false)
|
setStore("open", false)
|
||||||
@@ -214,26 +213,24 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
class="p-1"
|
class="p-1"
|
||||||
action={
|
action={
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Tooltip placement="top" forceMount={false} value={language.t("command.provider.connect")}>
|
<IconButton
|
||||||
<IconButton
|
icon="plus-small"
|
||||||
icon="plus-small"
|
variant="ghost"
|
||||||
variant="ghost"
|
iconSize="normal"
|
||||||
iconSize="normal"
|
class="size-6"
|
||||||
class="size-6"
|
aria-label={language.t("command.provider.connect")}
|
||||||
aria-label={language.t("command.provider.connect")}
|
title={language.t("command.provider.connect")}
|
||||||
onClick={handleConnectProvider}
|
onClick={handleConnectProvider}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
<IconButton
|
||||||
<Tooltip placement="top" forceMount={false} value={language.t("dialog.model.manage")}>
|
icon="sliders"
|
||||||
<IconButton
|
variant="ghost"
|
||||||
icon="sliders"
|
iconSize="normal"
|
||||||
variant="ghost"
|
class="size-6"
|
||||||
iconSize="normal"
|
aria-label={language.t("dialog.model.manage")}
|
||||||
class="size-6"
|
title={language.t("dialog.model.manage")}
|
||||||
aria-label={language.t("dialog.model.manage")}
|
onClick={handleManage}
|
||||||
onClick={handleManage}
|
/>
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,17 +5,9 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
|||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Tag } from "@opencode-ai/ui/tag"
|
import { Tag } from "@opencode-ai/ui/tag"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
|
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
|
||||||
|
|
||||||
const CUSTOM_ID = "_custom"
|
|
||||||
|
|
||||||
function icon(id: string): IconName {
|
|
||||||
if (iconNames.includes(id as IconName)) return id as IconName
|
|
||||||
return "synthetic"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DialogSelectProvider: Component = () => {
|
export const DialogSelectProvider: Component = () => {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@@ -34,13 +26,11 @@ export const DialogSelectProvider: Component = () => {
|
|||||||
key={(x) => x?.id}
|
key={(x) => x?.id}
|
||||||
items={() => {
|
items={() => {
|
||||||
language.locale()
|
language.locale()
|
||||||
return [{ id: CUSTOM_ID, name: "Custom provider" }, ...providers.all()]
|
return providers.all()
|
||||||
}}
|
}}
|
||||||
filterKeys={["id", "name"]}
|
filterKeys={["id", "name"]}
|
||||||
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
||||||
sortBy={(a, b) => {
|
sortBy={(a, b) => {
|
||||||
if (a.id === CUSTOM_ID) return -1
|
|
||||||
if (b.id === CUSTOM_ID) return 1
|
|
||||||
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
||||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
@@ -53,20 +43,13 @@ export const DialogSelectProvider: Component = () => {
|
|||||||
}}
|
}}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
if (!x) return
|
if (!x) return
|
||||||
if (x.id === CUSTOM_ID) {
|
|
||||||
dialog.show(() => <DialogCustomProvider back="providers" />)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
<div class="px-1.25 w-full flex items-center gap-x-3">
|
<div class="px-1.25 w-full flex items-center gap-x-3">
|
||||||
<ProviderIcon data-slot="list-item-extra-icon" id={icon(i.id)} />
|
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||||
<span>{i.name}</span>
|
<span>{i.name}</span>
|
||||||
<Show when={i.id === CUSTOM_ID}>
|
|
||||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
|
||||||
</Show>
|
|
||||||
<Show when={i.id === "opencode"}>
|
<Show when={i.id === "opencode"}>
|
||||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
|
|
||||||
type ServerStatus = { healthy: boolean; version?: string }
|
type ServerStatus = { healthy: boolean; version?: string }
|
||||||
|
|
||||||
@@ -41,11 +40,10 @@ interface EditRowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>): Promise<ServerStatus> {
|
async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>): Promise<ServerStatus> {
|
||||||
const signal = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(3000)
|
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: url,
|
baseUrl: url,
|
||||||
fetch: platform.fetch,
|
fetch: platform.fetch,
|
||||||
signal,
|
signal: AbortSignal.timeout(3000),
|
||||||
})
|
})
|
||||||
return sdk.global
|
return sdk.global
|
||||||
.health()
|
.health()
|
||||||
@@ -59,16 +57,18 @@ function AddRow(props: AddRowProps) {
|
|||||||
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"size-1.5 rounded-full absolute left-3 top-1/2 -translate-y-1/2 z-10 pointer-events-none": true,
|
"size-1.5 rounded-full absolute left-3 z-10 pointer-events-none": true,
|
||||||
"bg-icon-success-base": props.status === true,
|
"bg-icon-success-base": props.status === true,
|
||||||
"bg-icon-critical-base": props.status === false,
|
"bg-icon-critical-base": props.status === false,
|
||||||
"bg-border-weak-base": props.status === undefined,
|
"bg-border-weak-base": props.status === undefined,
|
||||||
}}
|
}}
|
||||||
|
style={{ top: "50%", transform: "translateY(-50%)" }}
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
// Position relative to input-wrapper
|
// Position relative to input-wrapper
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]')
|
const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]')
|
||||||
if (wrapper instanceof HTMLElement) {
|
if (wrapper instanceof HTMLElement) {
|
||||||
|
wrapper.style.position = "relative"
|
||||||
wrapper.appendChild(el)
|
wrapper.appendChild(el)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -149,18 +149,9 @@ export function DialogSelectServer() {
|
|||||||
})
|
})
|
||||||
const [defaultUrl, defaultUrlActions] = createResource(
|
const [defaultUrl, defaultUrlActions] = createResource(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
const url = await platform.getDefaultServerUrl?.()
|
||||||
const url = await platform.getDefaultServerUrl?.()
|
if (!url) return null
|
||||||
if (!url) return null
|
return normalizeServerUrl(url) ?? null
|
||||||
return normalizeServerUrl(url) ?? null
|
|
||||||
} catch (err) {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: err instanceof Error ? err.message : String(err),
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ initialValue: null },
|
{ initialValue: null },
|
||||||
)
|
)
|
||||||
@@ -517,16 +508,8 @@ export function DialogSelectServer() {
|
|||||||
<Show when={canDefault() && defaultUrl() !== i}>
|
<Show when={canDefault() && defaultUrl() !== i}>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={async () => {
|
onSelect={async () => {
|
||||||
try {
|
await platform.setDefaultServerUrl?.(i)
|
||||||
await platform.setDefaultServerUrl?.(i)
|
defaultUrlActions.mutate(i)
|
||||||
defaultUrlActions.mutate(i)
|
|
||||||
} catch (err) {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: err instanceof Error ? err.message : String(err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>
|
||||||
@@ -537,16 +520,8 @@ export function DialogSelectServer() {
|
|||||||
<Show when={canDefault() && defaultUrl() === i}>
|
<Show when={canDefault() && defaultUrl() === i}>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={async () => {
|
onSelect={async () => {
|
||||||
try {
|
await platform.setDefaultServerUrl?.(null)
|
||||||
await platform.setDefaultServerUrl?.(null)
|
defaultUrlActions.mutate(null)
|
||||||
defaultUrlActions.mutate(null)
|
|
||||||
} catch (err) {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: err instanceof Error ? err.message : String(err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
createMemo,
|
createMemo,
|
||||||
For,
|
For,
|
||||||
Match,
|
Match,
|
||||||
Show,
|
|
||||||
splitProps,
|
splitProps,
|
||||||
Switch,
|
Switch,
|
||||||
untrack,
|
untrack,
|
||||||
@@ -18,8 +17,6 @@ import {
|
|||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
type Kind = "add" | "del" | "mix"
|
|
||||||
|
|
||||||
type Filter = {
|
type Filter = {
|
||||||
files: Set<string>
|
files: Set<string>
|
||||||
dirs: Set<string>
|
dirs: Set<string>
|
||||||
@@ -29,11 +26,9 @@ export default function FileTree(props: {
|
|||||||
path: string
|
path: string
|
||||||
class?: string
|
class?: string
|
||||||
nodeClass?: string
|
nodeClass?: string
|
||||||
active?: string
|
|
||||||
level?: number
|
level?: number
|
||||||
allowed?: readonly string[]
|
allowed?: readonly string[]
|
||||||
modified?: readonly string[]
|
modified?: readonly string[]
|
||||||
kinds?: ReadonlyMap<string, Kind>
|
|
||||||
draggable?: boolean
|
draggable?: boolean
|
||||||
tooltip?: boolean
|
tooltip?: boolean
|
||||||
onFileClick?: (file: FileNode) => void
|
onFileClick?: (file: FileNode) => void
|
||||||
@@ -41,7 +36,6 @@ export default function FileTree(props: {
|
|||||||
_filter?: Filter
|
_filter?: Filter
|
||||||
_marks?: Set<string>
|
_marks?: Set<string>
|
||||||
_deeps?: Map<string, number>
|
_deeps?: Map<string, number>
|
||||||
_kinds?: ReadonlyMap<string, Kind>
|
|
||||||
}) {
|
}) {
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const level = props.level ?? 0
|
const level = props.level ?? 0
|
||||||
@@ -72,16 +66,9 @@ export default function FileTree(props: {
|
|||||||
const marks = createMemo(() => {
|
const marks = createMemo(() => {
|
||||||
if (props._marks) return props._marks
|
if (props._marks) return props._marks
|
||||||
|
|
||||||
const out = new Set<string>()
|
const modified = props.modified
|
||||||
for (const item of props.modified ?? []) out.add(item)
|
if (!modified || modified.length === 0) return
|
||||||
for (const item of props.kinds?.keys() ?? []) out.add(item)
|
return new Set(modified)
|
||||||
if (out.size === 0) return
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
|
|
||||||
const kinds = createMemo(() => {
|
|
||||||
if (props._kinds) return props._kinds
|
|
||||||
return props.kinds
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const deeps = createMemo(() => {
|
const deeps = createMemo(() => {
|
||||||
@@ -149,8 +136,7 @@ export default function FileTree(props: {
|
|||||||
<Dynamic
|
<Dynamic
|
||||||
component={local.as ?? "div"}
|
component={local.as ?? "div"}
|
||||||
classList={{
|
classList={{
|
||||||
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
|
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-2 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
|
||||||
"bg-surface-base-active": local.node.path === props.active,
|
|
||||||
...(local.classList ?? {}),
|
...(local.classList ?? {}),
|
||||||
[local.class ?? ""]: !!local.class,
|
[local.class ?? ""]: !!local.class,
|
||||||
[props.nodeClass ?? ""]: !!props.nodeClass,
|
[props.nodeClass ?? ""]: !!props.nodeClass,
|
||||||
@@ -184,65 +170,18 @@ export default function FileTree(props: {
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{local.children}
|
{local.children}
|
||||||
{(() => {
|
<span
|
||||||
const kind = kinds()?.get(local.node.path)
|
classList={{
|
||||||
const marked = marks()?.has(local.node.path) ?? false
|
"flex-1 min-w-0 text-12-medium whitespace-nowrap truncate": true,
|
||||||
const active = !!kind && marked && !local.node.ignored
|
"text-text-weaker": local.node.ignored,
|
||||||
const color =
|
"text-text-weak": !local.node.ignored,
|
||||||
kind === "add"
|
}}
|
||||||
? "color: var(--icon-diff-add-base)"
|
>
|
||||||
: kind === "del"
|
{local.node.name}
|
||||||
? "color: var(--icon-diff-delete-base)"
|
</span>
|
||||||
: kind === "mix"
|
{local.node.type === "file" && marks()?.has(local.node.path) ? (
|
||||||
? "color: var(--icon-diff-modified-base)"
|
<div class="shrink-0 size-1.5 rounded-full bg-surface-warning-strong" />
|
||||||
: undefined
|
) : null}
|
||||||
return (
|
|
||||||
<span
|
|
||||||
classList={{
|
|
||||||
"flex-1 min-w-0 text-12-medium whitespace-nowrap truncate": true,
|
|
||||||
"text-text-weaker": local.node.ignored,
|
|
||||||
"text-text-weak": !local.node.ignored && !active,
|
|
||||||
}}
|
|
||||||
style={active ? color : undefined}
|
|
||||||
>
|
|
||||||
{local.node.name}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
{(() => {
|
|
||||||
const kind = kinds()?.get(local.node.path)
|
|
||||||
if (!kind) return null
|
|
||||||
if (!marks()?.has(local.node.path)) return null
|
|
||||||
|
|
||||||
if (local.node.type === "file") {
|
|
||||||
const text = kind === "add" ? "A" : kind === "del" ? "D" : "M"
|
|
||||||
const color =
|
|
||||||
kind === "add"
|
|
||||||
? "color: var(--icon-diff-add-base)"
|
|
||||||
: kind === "del"
|
|
||||||
? "color: var(--icon-diff-delete-base)"
|
|
||||||
: "color: var(--icon-diff-modified-base)"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span class="shrink-0 w-4 text-center text-12-medium" style={color}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (local.node.type === "directory") {
|
|
||||||
const color =
|
|
||||||
kind === "add"
|
|
||||||
? "background-color: var(--icon-diff-add-base)"
|
|
||||||
: kind === "del"
|
|
||||||
? "background-color: var(--icon-diff-delete-base)"
|
|
||||||
: "background-color: var(--icon-diff-modified-base)"
|
|
||||||
|
|
||||||
return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={color} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
})()}
|
|
||||||
</Dynamic>
|
</Dynamic>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -255,56 +194,8 @@ export default function FileTree(props: {
|
|||||||
const deep = () => deeps().get(node.path) ?? -1
|
const deep = () => deeps().get(node.path) ?? -1
|
||||||
const Wrapper = (p: ParentProps) => {
|
const Wrapper = (p: ParentProps) => {
|
||||||
if (!tooltip()) return p.children
|
if (!tooltip()) return p.children
|
||||||
|
|
||||||
const parts = node.path.split("/")
|
|
||||||
const leaf = parts[parts.length - 1] ?? node.path
|
|
||||||
const head = parts.slice(0, -1).join("/")
|
|
||||||
const prefix = head ? `${head}/` : ""
|
|
||||||
|
|
||||||
const kind = () => kinds()?.get(node.path)
|
|
||||||
const label = () => {
|
|
||||||
const k = kind()
|
|
||||||
if (!k) return
|
|
||||||
if (k === "add") return "Additions"
|
|
||||||
if (k === "del") return "Deletions"
|
|
||||||
return "Modifications"
|
|
||||||
}
|
|
||||||
|
|
||||||
const ignored = () => node.type === "directory" && node.ignored
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right" class="w-full">
|
||||||
forceMount={false}
|
|
||||||
openDelay={2000}
|
|
||||||
placement="bottom-start"
|
|
||||||
class="w-full"
|
|
||||||
contentStyle={{ "max-width": "480px", width: "fit-content" }}
|
|
||||||
value={
|
|
||||||
<div class="flex items-center min-w-0 whitespace-nowrap text-12-regular">
|
|
||||||
<span
|
|
||||||
class="min-w-0 truncate text-text-invert-base"
|
|
||||||
style={{ direction: "rtl", "unicode-bidi": "plaintext" }}
|
|
||||||
>
|
|
||||||
{prefix}
|
|
||||||
</span>
|
|
||||||
<span class="shrink-0 text-text-invert-strong">{leaf}</span>
|
|
||||||
<Show when={label()}>
|
|
||||||
{(t: () => string) => (
|
|
||||||
<>
|
|
||||||
<span class="mx-1 font-bold text-text-invert-strong">•</span>
|
|
||||||
<span class="shrink-0 text-text-invert-strong">{t()}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<Show when={ignored()}>
|
|
||||||
<>
|
|
||||||
<span class="mx-1 font-bold text-text-invert-strong">•</span>
|
|
||||||
<span class="shrink-0 text-text-invert-strong">Ignored</span>
|
|
||||||
</>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{p.children}
|
{p.children}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
@@ -344,15 +235,12 @@ export default function FileTree(props: {
|
|||||||
level={level + 1}
|
level={level + 1}
|
||||||
allowed={props.allowed}
|
allowed={props.allowed}
|
||||||
modified={props.modified}
|
modified={props.modified}
|
||||||
kinds={props.kinds}
|
|
||||||
active={props.active}
|
|
||||||
draggable={props.draggable}
|
draggable={props.draggable}
|
||||||
tooltip={props.tooltip}
|
tooltip={props.tooltip}
|
||||||
onFileClick={props.onFileClick}
|
onFileClick={props.onFileClick}
|
||||||
_filter={filter()}
|
_filter={filter()}
|
||||||
_marks={marks()}
|
_marks={marks()}
|
||||||
_deeps={deeps()}
|
_deeps={deeps()}
|
||||||
_kinds={kinds()}
|
|
||||||
/>
|
/>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
|
||||||
const commentInReview = (path: string) => {
|
const commentInReview = (path: string) => {
|
||||||
const sessionID = params.id
|
const sessionID = params.id
|
||||||
@@ -186,17 +187,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const focus = { file: item.path, id: item.commentID }
|
const focus = { file: item.path, id: item.commentID }
|
||||||
comments.setActive(focus)
|
comments.setActive(focus)
|
||||||
|
view().reviewPanel.open()
|
||||||
|
|
||||||
const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
|
if (item.commentOrigin === "review") {
|
||||||
if (wantsReview) {
|
tabs().open("review")
|
||||||
layout.fileTree.open()
|
requestAnimationFrame(() => comments.setFocus(focus))
|
||||||
layout.fileTree.setTab("changes")
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.commentOrigin !== "file" && commentInReview(item.path)) {
|
||||||
|
tabs().open("review")
|
||||||
requestAnimationFrame(() => comments.setFocus(focus))
|
requestAnimationFrame(() => comments.setFocus(focus))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
layout.fileTree.open()
|
|
||||||
layout.fileTree.setTab("all")
|
|
||||||
const tab = files.tab(item.path)
|
const tab = files.tab(item.path)
|
||||||
tabs().open(tab)
|
tabs().open(tab)
|
||||||
files.load(item.path)
|
files.load(item.path)
|
||||||
@@ -1038,17 +1042,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
|
|
||||||
|
|
||||||
if (store.popover) {
|
if (store.popover) {
|
||||||
if (event.key === "Tab") {
|
if (event.key === "Tab") {
|
||||||
selectPopoverActive()
|
selectPopoverActive()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter"
|
if (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter") {
|
||||||
const ctrlNav = ctrl && (event.key === "n" || event.key === "p")
|
|
||||||
if (nav || ctrlNav) {
|
|
||||||
if (store.popover === "at") {
|
if (store.popover === "at") {
|
||||||
atOnKeyDown(event)
|
atOnKeyDown(event)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -1062,6 +1062,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
|
||||||
|
|
||||||
if (ctrl && event.code === "KeyG") {
|
if (ctrl && event.code === "KeyG") {
|
||||||
if (store.popover) {
|
if (store.popover) {
|
||||||
setStore("popover", null)
|
setStore("popover", null)
|
||||||
@@ -1561,17 +1563,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const timeoutMs = 5 * 60 * 1000
|
const timeoutMs = 5 * 60 * 1000
|
||||||
const timer = { id: undefined as number | undefined }
|
|
||||||
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||||
timer.id = window.setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") })
|
resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") })
|
||||||
}, timeoutMs)
|
}, timeoutMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout]).finally(() => {
|
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout])
|
||||||
if (timer.id === undefined) return
|
|
||||||
clearTimeout(timer.id)
|
|
||||||
})
|
|
||||||
pending.delete(session.id)
|
pending.delete(session.id)
|
||||||
if (controller.signal.aborted) return false
|
if (controller.signal.aborted) return false
|
||||||
if (result.status === "failed") throw new Error(result.message)
|
if (result.status === "failed") throw new Error(result.message)
|
||||||
@@ -1748,7 +1746,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
value={
|
value={
|
||||||
<span class="flex max-w-[300px]">
|
<span class="flex max-w-[300px]">
|
||||||
<span class="text-text-invert-base truncate-start [unicode-bidi:plaintext] min-w-0">
|
<span
|
||||||
|
class="text-text-invert-base truncate min-w-0"
|
||||||
|
style={{ direction: "rtl", "text-align": "left", "unicode-bidi": "plaintext" }}
|
||||||
|
>
|
||||||
{getDirectory(item.path)}
|
{getDirectory(item.path)}
|
||||||
</span>
|
</span>
|
||||||
<span class="shrink-0">{getFilename(item.path)}</span>
|
<span class="shrink-0">{getFilename(item.path)}</span>
|
||||||
@@ -1771,7 +1772,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
|
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
|
||||||
<div class="flex items-center text-11-regular min-w-0 font-medium">
|
<div
|
||||||
|
class="flex items-center text-11-regular min-w-0"
|
||||||
|
style={{ "font-weight": "var(--font-weight-medium)" }}
|
||||||
|
>
|
||||||
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
|
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
|
||||||
<Show when={item.selection}>
|
<Show when={item.selection}>
|
||||||
{(sel) => (
|
{(sel) => (
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
const variant = createMemo(() => props.variant ?? "button")
|
const variant = createMemo(() => props.variant ?? "button")
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||||
|
|
||||||
const usd = createMemo(
|
const usd = createMemo(
|
||||||
@@ -57,8 +58,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
|
|
||||||
const openContext = () => {
|
const openContext = () => {
|
||||||
if (!params.id) return
|
if (!params.id) return
|
||||||
layout.fileTree.open()
|
view().reviewPanel.open()
|
||||||
layout.fileTree.setTab("all")
|
|
||||||
tabs().open("context")
|
tabs().open("context")
|
||||||
tabs().setActive("context")
|
tabs().setActive("context")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { usePlatform } from "@/context/platform"
|
|||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
@@ -29,7 +29,7 @@ export function SessionHeader() {
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||||
const project = createMemo(() => {
|
const project = createMemo(() => {
|
||||||
const directory = projectDirectory()
|
const directory = projectDirectory()
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
@@ -45,6 +45,7 @@ export function SessionHeader() {
|
|||||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
||||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||||
const showShare = createMemo(() => shareEnabled() && !!currentSession())
|
const showShare = createMemo(() => shareEnabled() && !!currentSession())
|
||||||
|
const showReview = createMemo(() => !!currentSession())
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const view = createMemo(() => layout.view(sessionKey))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
|
||||||
@@ -283,32 +284,59 @@ export function SessionHeader() {
|
|||||||
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
|
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="group/file-tree-toggle size-6 p-0"
|
class="group/review-toggle size-6 p-0"
|
||||||
onClick={() => layout.fileTree.toggle()}
|
onClick={() => view().reviewPanel.toggle()}
|
||||||
aria-label={language.t("command.review.toggle")}
|
aria-label={language.t("command.review.toggle")}
|
||||||
aria-expanded={layout.fileTree.opened()}
|
aria-expanded={view().reviewPanel.opened()}
|
||||||
aria-controls="review-panel"
|
aria-controls="review-panel"
|
||||||
|
tabIndex={showReview() ? 0 : -1}
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||||
<Icon
|
<Icon
|
||||||
size="small"
|
size="small"
|
||||||
name={layout.fileTree.opened() ? "layout-right-full" : "layout-right"}
|
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
|
||||||
class="group-hover/file-tree-toggle:hidden"
|
class="group-hover/review-toggle:hidden"
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
size="small"
|
size="small"
|
||||||
name="layout-right-partial"
|
name="layout-right-partial"
|
||||||
class="hidden group-hover/file-tree-toggle:inline-block"
|
class="hidden group-hover/review-toggle:inline-block"
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
size="small"
|
size="small"
|
||||||
name={layout.fileTree.opened() ? "layout-right" : "layout-right-full"}
|
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
|
||||||
class="hidden group-active/file-tree-toggle:inline-block"
|
class="hidden group-active/review-toggle:inline-block"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hidden md:block shrink-0">
|
||||||
|
<Tooltip value="Toggle file tree" placement="bottom">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="group/file-tree-toggle size-6 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
const opening = !layout.fileTree.opened()
|
||||||
|
if (opening && !view().reviewPanel.opened()) view().reviewPanel.open()
|
||||||
|
layout.fileTree.toggle()
|
||||||
|
}}
|
||||||
|
aria-label="Toggle file tree"
|
||||||
|
aria-expanded={layout.fileTree.opened()}
|
||||||
|
>
|
||||||
|
<div class="relative flex items-center justify-center size-4">
|
||||||
|
<Icon
|
||||||
|
size="small"
|
||||||
|
name="bullet-list"
|
||||||
|
classList={{
|
||||||
|
"text-icon-strong": layout.fileTree.opened(),
|
||||||
|
"text-icon-weak": !layout.fileTree.opened(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ export function NewSessionView(props: NewSessionViewProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6 pb-[calc(var(--prompt-height,11.25rem)+64px)]">
|
<div
|
||||||
|
class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6"
|
||||||
|
style={{ "padding-bottom": "calc(var(--prompt-height, 11.25rem) + 64px)" }}
|
||||||
|
>
|
||||||
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
|
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
|
||||||
<div class="flex justify-center items-center gap-3">
|
<div class="flex justify-center items-center gap-3">
|
||||||
<Icon name="folder" size="small" />
|
<Icon name="folder" size="small" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
|
|
||||||
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-x-1.5 min-w-0">
|
<div class="flex items-center gap-x-1.5">
|
||||||
<FileIcon
|
<FileIcon
|
||||||
node={{ path: props.path, type: "file" }}
|
node={{ path: props.path, type: "file" }}
|
||||||
classList={{
|
classList={{
|
||||||
@@ -19,7 +19,7 @@ export function FileVisual(props: { path: string; active?: boolean }): JSX.Eleme
|
|||||||
"grayscale-0": props.active,
|
"grayscale-0": props.active,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span class="text-14-medium truncate">{getFilename(props.path)}</span>
|
<span class="text-14-medium">{getFilename(props.path)}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -38,9 +38,8 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
|||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="close-small"
|
icon="close"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-5 w-5"
|
|
||||||
onClick={() => props.onTabClose(props.tab)}
|
onClick={() => props.onTabClose(props.tab)}
|
||||||
aria-label={language.t("common.closeTab")}
|
aria-label={language.t("common.closeTab")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span onDblClick={edit} classList={{ invisible: store.editing }}>
|
<span onDblClick={edit} style={{ visibility: store.editing ? "hidden" : "visible" }}>
|
||||||
{label()}
|
{label()}
|
||||||
</span>
|
</span>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
@@ -167,8 +167,8 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
<DropdownMenu open={store.menuOpen} onOpenChange={(open) => setStore("menuOpen", open)}>
|
<DropdownMenu open={store.menuOpen} onOpenChange={(open) => setStore("menuOpen", open)}>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="fixed"
|
|
||||||
style={{
|
style={{
|
||||||
|
position: "fixed",
|
||||||
left: `${store.menuPosition.x}px`,
|
left: `${store.menuPosition.x}px`,
|
||||||
top: `${store.menuPosition.y}px`,
|
top: `${store.menuPosition.y}px`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { Component, createMemo, type JSX } from "solid-js"
|
import { Component, createMemo, type JSX } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
import { useSettings, monoFontFamily } from "@/context/settings"
|
||||||
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
|
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
|
||||||
import { Link } from "./link"
|
import { Link } from "./link"
|
||||||
@@ -33,67 +29,8 @@ const playDemoSound = (src: string) => {
|
|||||||
export const SettingsGeneral: Component = () => {
|
export const SettingsGeneral: Component = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const platform = usePlatform()
|
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
checking: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const check = () => {
|
|
||||||
if (!platform.checkUpdate) return
|
|
||||||
setStore("checking", true)
|
|
||||||
|
|
||||||
void platform
|
|
||||||
.checkUpdate()
|
|
||||||
.then((result) => {
|
|
||||||
if (!result.updateAvailable) {
|
|
||||||
showToast({
|
|
||||||
variant: "success",
|
|
||||||
icon: "circle-check",
|
|
||||||
title: language.t("settings.updates.toast.latest.title"),
|
|
||||||
description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions =
|
|
||||||
platform.update && platform.restart
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: language.t("toast.update.action.installRestart"),
|
|
||||||
onClick: async () => {
|
|
||||||
await platform.update!()
|
|
||||||
await platform.restart!()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: language.t("toast.update.action.notYet"),
|
|
||||||
onClick: "dismiss" as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
label: language.t("toast.update.action.notYet"),
|
|
||||||
onClick: "dismiss" as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
showToast({
|
|
||||||
persistent: true,
|
|
||||||
icon: "download",
|
|
||||||
title: language.t("toast.update.title"),
|
|
||||||
description: language.t("toast.update.description", { version: result.version ?? "" }),
|
|
||||||
actions,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
|
||||||
})
|
|
||||||
.finally(() => setStore("checking", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeOptions = createMemo(() =>
|
const themeOptions = createMemo(() =>
|
||||||
Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
|
Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
|
||||||
)
|
)
|
||||||
@@ -130,8 +67,14 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const soundOptions = [...SOUND_OPTIONS]
|
const soundOptions = [...SOUND_OPTIONS]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div
|
||||||
|
class="sticky top-0 z-10"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-1 pt-6 pb-8">
|
<div class="flex flex-col gap-1 pt-6 pb-8">
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,6 +214,23 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Updates Section */}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
|
||||||
|
|
||||||
|
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.releaseNotes.title")}
|
||||||
|
description={language.t("settings.general.row.releaseNotes.description")}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.releaseNotes()}
|
||||||
|
onChange={(checked) => settings.general.setReleaseNotes(checked)}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Sound effects Section */}
|
{/* Sound effects Section */}
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
|
||||||
@@ -349,50 +309,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Updates Section */}
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
|
|
||||||
|
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.updates.row.startup.title")}
|
|
||||||
description={language.t("settings.updates.row.startup.description")}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
checked={settings.updates.startup()}
|
|
||||||
disabled={!platform.checkUpdate}
|
|
||||||
onChange={(checked) => settings.updates.setStartup(checked)}
|
|
||||||
/>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.releaseNotes.title")}
|
|
||||||
description={language.t("settings.general.row.releaseNotes.description")}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.releaseNotes()}
|
|
||||||
onChange={(checked) => settings.general.setReleaseNotes(checked)}
|
|
||||||
/>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.updates.row.check.title")}
|
|
||||||
description={language.t("settings.updates.row.check.description")}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="secondary"
|
|
||||||
disabled={store.checking || !platform.checkUpdate}
|
|
||||||
onClick={check}
|
|
||||||
>
|
|
||||||
{store.checking
|
|
||||||
? language.t("settings.updates.action.checking")
|
|
||||||
: language.t("settings.updates.action.checkNow")}
|
|
||||||
</Button>
|
|
||||||
</SettingsRow>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -406,8 +322,8 @@ interface SettingsRowProps {
|
|||||||
|
|
||||||
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||||
<div class="flex flex-col gap-0.5 min-w-0">
|
<div class="flex flex-col gap-0.5">
|
||||||
<span class="text-14-medium text-text-strong">{props.title}</span>
|
<span class="text-14-medium text-text-strong">{props.title}</span>
|
||||||
<span class="text-12-regular text-text-weak">{props.description}</span>
|
<span class="text-12-regular text-text-weak">{props.description}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -352,8 +352,14 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div
|
||||||
|
class="sticky top-0 z-10"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
||||||
|
|||||||
@@ -39,8 +39,14 @@ export const SettingsModels: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div
|
||||||
|
class="sticky top-0 z-10"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||||
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
||||||
@@ -99,7 +105,7 @@ export const SettingsModels: Component = () => {
|
|||||||
{(item) => {
|
{(item) => {
|
||||||
const key = { providerID: item.provider.id, modelID: item.id }
|
const key = { providerID: item.provider.id, modelID: item.id }
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<span class="text-14-regular text-text-strong truncate block">{item.name}</span>
|
<span class="text-14-regular text-text-strong truncate block">{item.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -175,14 +175,20 @@ export const SettingsPermissions: Component = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div
|
||||||
<div class="flex flex-col gap-1 px-4 py-8 sm:p-8 max-w-[720px]">
|
class="sticky top-0 z-10"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-1 p-8 max-w-[720px]">
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.permissions.title")}</h2>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.permissions.title")}</h2>
|
||||||
<p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
|
<p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-6 px-4 py-6 sm:p-8 sm:pt-6 max-w-[720px]">
|
<div class="flex flex-col gap-6 p-8 pt-6 max-w-[720px]">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3>
|
<h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3>
|
||||||
<div class="border border-border-weak-base rounded-lg overflow-hidden">
|
<div class="border border-border-weak-base rounded-lg overflow-hidden">
|
||||||
@@ -217,8 +223,8 @@ interface SettingsRowProps {
|
|||||||
|
|
||||||
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
|
<div class="flex items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
|
||||||
<div class="flex flex-col gap-0.5 min-w-0">
|
<div class="flex flex-col gap-0.5">
|
||||||
<span class="text-14-medium text-text-strong">{props.title}</span>
|
<span class="text-14-medium text-text-strong">{props.title}</span>
|
||||||
<span class="text-12-regular text-text-weak">{props.description}</span>
|
<span class="text-12-regular text-text-weak">{props.description}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Tag } from "@opencode-ai/ui/tag"
|
import { Tag } from "@opencode-ai/ui/tag"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
|
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||||
import { createMemo, type Component, For, Show } from "solid-js"
|
import { createMemo, type Component, For, Show } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
|
||||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
|
||||||
|
|
||||||
type ProviderSource = "env" | "api" | "config" | "custom"
|
type ProviderSource = "env" | "api" | "config" | "custom"
|
||||||
type ProviderMeta = { source?: ProviderSource }
|
type ProviderMeta = { source?: ProviderSource }
|
||||||
@@ -20,14 +18,8 @@ export const SettingsProviders: Component = () => {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const globalSync = useGlobalSync()
|
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
|
|
||||||
const icon = (id: string): IconName => {
|
|
||||||
if (iconNames.includes(id as IconName)) return id as IconName
|
|
||||||
return "synthetic"
|
|
||||||
}
|
|
||||||
|
|
||||||
const connected = createMemo(() => {
|
const connected = createMemo(() => {
|
||||||
return providers
|
return providers
|
||||||
.connected()
|
.connected()
|
||||||
@@ -50,53 +42,14 @@ export const SettingsProviders: Component = () => {
|
|||||||
const current = source(item)
|
const current = source(item)
|
||||||
if (current === "env") return language.t("settings.providers.tag.environment")
|
if (current === "env") return language.t("settings.providers.tag.environment")
|
||||||
if (current === "api") return language.t("provider.connect.method.apiKey")
|
if (current === "api") return language.t("provider.connect.method.apiKey")
|
||||||
if (current === "config") {
|
if (current === "config") return language.t("settings.providers.tag.config")
|
||||||
const id = (item as { id?: string }).id
|
|
||||||
if (id && isConfigCustom(id)) return language.t("settings.providers.tag.custom")
|
|
||||||
return language.t("settings.providers.tag.config")
|
|
||||||
}
|
|
||||||
if (current === "custom") return language.t("settings.providers.tag.custom")
|
if (current === "custom") return language.t("settings.providers.tag.custom")
|
||||||
return language.t("settings.providers.tag.other")
|
return language.t("settings.providers.tag.other")
|
||||||
}
|
}
|
||||||
|
|
||||||
const canDisconnect = (item: unknown) => source(item) !== "env"
|
const canDisconnect = (item: unknown) => source(item) !== "env"
|
||||||
|
|
||||||
const isConfigCustom = (providerID: string) => {
|
|
||||||
const provider = globalSync.data.config.provider?.[providerID]
|
|
||||||
if (!provider) return false
|
|
||||||
if (provider.npm !== "@ai-sdk/openai-compatible") return false
|
|
||||||
if (!provider.models || Object.keys(provider.models).length === 0) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const disableProvider = async (providerID: string, name: string) => {
|
|
||||||
const before = globalSync.data.config.disabled_providers ?? []
|
|
||||||
const next = before.includes(providerID) ? before : [...before, providerID]
|
|
||||||
globalSync.set("config", "disabled_providers", next)
|
|
||||||
|
|
||||||
await globalSync
|
|
||||||
.updateConfig({ disabled_providers: next })
|
|
||||||
.then(() => {
|
|
||||||
showToast({
|
|
||||||
variant: "success",
|
|
||||||
icon: "circle-check",
|
|
||||||
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
|
|
||||||
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
globalSync.set("config", "disabled_providers", before)
|
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const disconnect = async (providerID: string, name: string) => {
|
const disconnect = async (providerID: string, name: string) => {
|
||||||
if (isConfigCustom(providerID)) {
|
|
||||||
await globalSDK.client.auth.remove({ providerID }).catch(() => undefined)
|
|
||||||
await disableProvider(providerID, name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await globalSDK.client.auth
|
await globalSDK.client.auth
|
||||||
.remove({ providerID })
|
.remove({ providerID })
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -115,7 +68,7 @@ export const SettingsProviders: Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
|
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
||||||
@@ -136,9 +89,9 @@ export const SettingsProviders: Component = () => {
|
|||||||
>
|
>
|
||||||
<For each={connected()}>
|
<For each={connected()}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div class="group flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
|
<div class="group flex items-center justify-between gap-4 h-16 border-b border-border-weak-base last:border-none">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||||
<span class="text-14-medium text-text-strong truncate">{item.name}</span>
|
<span class="text-14-medium text-text-strong truncate">{item.name}</span>
|
||||||
<Tag>{type(item)}</Tag>
|
<Tag>{type(item)}</Tag>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,10 +119,10 @@ export const SettingsProviders: Component = () => {
|
|||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||||
<For each={popular()}>
|
<For each={popular()}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
|
<div class="flex items-center justify-between gap-4 h-16 border-b border-border-weak-base last:border-none">
|
||||||
<div class="flex flex-col min-w-0">
|
<div class="flex flex-col min-w-0">
|
||||||
<div class="flex items-center gap-x-3">
|
<div class="flex items-center gap-x-3">
|
||||||
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||||
<span class="text-14-medium text-text-strong">{item.name}</span>
|
<span class="text-14-medium text-text-strong">{item.name}</span>
|
||||||
<Show when={item.id === "opencode"}>
|
<Show when={item.id === "opencode"}>
|
||||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||||
@@ -224,27 +177,6 @@ export const SettingsProviders: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4 h-16 border-b border-border-weak-base last:border-none">
|
|
||||||
<div class="flex flex-col min-w-0">
|
|
||||||
<div class="flex items-center gap-x-3">
|
|
||||||
<ProviderIcon id={icon("synthetic")} class="size-5 shrink-0 icon-strong-base" />
|
|
||||||
<span class="text-14-medium text-text-strong">Custom provider</span>
|
|
||||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
|
||||||
</div>
|
|
||||||
<span class="text-12-regular text-text-weak pl-8">Add an OpenAI-compatible provider by base URL.</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="secondary"
|
|
||||||
icon="plus-small"
|
|
||||||
onClick={() => {
|
|
||||||
dialog.show(() => <DialogCustomProvider back="close" />)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language.t("common.connect")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -15,16 +15,14 @@ import { usePlatform } from "@/context/platform"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { DialogSelectServer } from "./dialog-select-server"
|
import { DialogSelectServer } from "./dialog-select-server"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
|
|
||||||
type ServerStatus = { healthy: boolean; version?: string }
|
type ServerStatus = { healthy: boolean; version?: string }
|
||||||
|
|
||||||
async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>): Promise<ServerStatus> {
|
async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>): Promise<ServerStatus> {
|
||||||
const signal = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(3000)
|
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: url,
|
baseUrl: url,
|
||||||
fetch: platform.fetch,
|
fetch: platform.fetch,
|
||||||
signal,
|
signal: AbortSignal.timeout(3000),
|
||||||
})
|
})
|
||||||
return sdk.global
|
return sdk.global
|
||||||
.health()
|
.health()
|
||||||
@@ -102,21 +100,15 @@ export function StatusPopover() {
|
|||||||
const toggleMcp = async (name: string) => {
|
const toggleMcp = async (name: string) => {
|
||||||
if (store.loading) return
|
if (store.loading) return
|
||||||
setStore("loading", name)
|
setStore("loading", name)
|
||||||
|
const status = sync.data.mcp[name]
|
||||||
try {
|
if (status?.status === "connected") {
|
||||||
const status = sync.data.mcp[name]
|
await sdk.client.mcp.disconnect({ name })
|
||||||
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
} else {
|
||||||
const result = await sdk.client.mcp.status()
|
await sdk.client.mcp.connect({ name })
|
||||||
if (result.data) sync.set("mcp", result.data)
|
|
||||||
} catch (err) {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: err instanceof Error ? err.message : String(err),
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setStore("loading", null)
|
|
||||||
}
|
}
|
||||||
|
const result = await sdk.client.mcp.status()
|
||||||
|
if (result.data) sync.set("mcp", result.data)
|
||||||
|
setStore("loading", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
||||||
@@ -176,16 +168,33 @@ export function StatusPopover() {
|
|||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
shift={-136}
|
shift={-136}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1 w-[360px] rounded-xl shadow-[var(--shadow-lg-border-base)]">
|
<div
|
||||||
|
class="flex items-center gap-1 w-[360px] rounded-xl"
|
||||||
|
style={{ "box-shadow": "var(--shadow-lg-border-base)" }}
|
||||||
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
aria-label={language.t("status.popover.ariaLabel")}
|
aria-label={language.t("status.popover.ariaLabel")}
|
||||||
class="tabs bg-background-strong rounded-xl overflow-hidden"
|
class="tabs"
|
||||||
data-component="tabs"
|
data-component="tabs"
|
||||||
data-active="servers"
|
data-active="servers"
|
||||||
defaultValue="servers"
|
defaultValue="servers"
|
||||||
variant="alt"
|
variant="alt"
|
||||||
|
style={{
|
||||||
|
"background-color": "var(--background-strong)",
|
||||||
|
"border-radius": "12px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
<Tabs.List
|
||||||
|
data-slot="tablist"
|
||||||
|
style={{
|
||||||
|
"background-color": "transparent",
|
||||||
|
"border-bottom": "none",
|
||||||
|
padding: "8px 16px 0",
|
||||||
|
gap: "16px",
|
||||||
|
height: "40px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
||||||
{serverCount() > 0 ? `${serverCount()} ` : ""}
|
{serverCount() > 0 ? `${serverCount()} ` : ""}
|
||||||
{language.t("status.popover.tab.servers")}
|
{language.t("status.popover.tab.servers")}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { monoFontFamily, useSettings } from "@/context/settings"
|
|||||||
import { SerializeAddon } from "@/addons/serialize"
|
import { SerializeAddon } from "@/addons/serialize"
|
||||||
import { LocalPTY } from "@/context/terminal"
|
import { LocalPTY } from "@/context/terminal"
|
||||||
import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
|
import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
|
|
||||||
export interface TerminalProps extends ComponentProps<"div"> {
|
export interface TerminalProps extends ComponentProps<"div"> {
|
||||||
pty: LocalPTY
|
pty: LocalPTY
|
||||||
@@ -16,19 +14,6 @@ export interface TerminalProps extends ComponentProps<"div"> {
|
|||||||
onConnectError?: (error: unknown) => void
|
onConnectError?: (error: unknown) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let shared: Promise<{ mod: typeof import("ghostty-web"); ghostty: Ghostty }> | undefined
|
|
||||||
|
|
||||||
const loadGhostty = () => {
|
|
||||||
if (shared) return shared
|
|
||||||
shared = import("ghostty-web")
|
|
||||||
.then(async (mod) => ({ mod, ghostty: await mod.Ghostty.load() }))
|
|
||||||
.catch((err) => {
|
|
||||||
shared = undefined
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
return shared
|
|
||||||
}
|
|
||||||
|
|
||||||
type TerminalColors = {
|
type TerminalColors = {
|
||||||
background: string
|
background: string
|
||||||
foreground: string
|
foreground: string
|
||||||
@@ -55,7 +40,6 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
|
||||||
let ws: WebSocket | undefined
|
let ws: WebSocket | undefined
|
||||||
@@ -66,20 +50,8 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
let handleResize: () => void
|
let handleResize: () => void
|
||||||
let handleTextareaFocus: () => void
|
let handleTextareaFocus: () => void
|
||||||
let handleTextareaBlur: () => void
|
let handleTextareaBlur: () => void
|
||||||
|
let reconnect: number | undefined
|
||||||
let disposed = false
|
let disposed = false
|
||||||
const cleanups: VoidFunction[] = []
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
if (!cleanups.length) return
|
|
||||||
const fns = cleanups.splice(0).reverse()
|
|
||||||
for (const fn of fns) {
|
|
||||||
try {
|
|
||||||
fn()
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTerminalColors = (): TerminalColors => {
|
const getTerminalColors = (): TerminalColors => {
|
||||||
const mode = theme.mode()
|
const mode = theme.mode()
|
||||||
@@ -135,237 +107,188 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
focusTerminal()
|
focusTerminal()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
const run = async () => {
|
const mod = await import("ghostty-web")
|
||||||
const loaded = await loadGhostty()
|
ghostty = await mod.Ghostty.load()
|
||||||
if (disposed) return
|
|
||||||
|
|
||||||
const mod = loaded.mod
|
const once = { value: false }
|
||||||
const g = loaded.ghostty
|
|
||||||
|
|
||||||
const once = { value: false }
|
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
||||||
|
if (window.__OPENCODE__?.serverPassword) {
|
||||||
|
url.username = "opencode"
|
||||||
|
url.password = window.__OPENCODE__?.serverPassword
|
||||||
|
}
|
||||||
|
const socket = new WebSocket(url)
|
||||||
|
ws = socket
|
||||||
|
|
||||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
const t = new mod.Terminal({
|
||||||
if (window.__OPENCODE__?.serverPassword) {
|
cursorBlink: true,
|
||||||
url.username = "opencode"
|
cursorStyle: "bar",
|
||||||
url.password = window.__OPENCODE__?.serverPassword
|
fontSize: 14,
|
||||||
|
fontFamily: monoFontFamily(settings.appearance.font()),
|
||||||
|
allowTransparency: true,
|
||||||
|
theme: terminalColors(),
|
||||||
|
scrollback: 10_000,
|
||||||
|
ghostty,
|
||||||
|
})
|
||||||
|
term = t
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
const selection = t.getSelection()
|
||||||
|
if (!selection) return false
|
||||||
|
|
||||||
|
const body = document.body
|
||||||
|
if (body) {
|
||||||
|
const textarea = document.createElement("textarea")
|
||||||
|
textarea.value = selection
|
||||||
|
textarea.setAttribute("readonly", "")
|
||||||
|
textarea.style.position = "fixed"
|
||||||
|
textarea.style.opacity = "0"
|
||||||
|
body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
const copied = document.execCommand("copy")
|
||||||
|
body.removeChild(textarea)
|
||||||
|
if (copied) return true
|
||||||
}
|
}
|
||||||
const socket = new WebSocket(url)
|
|
||||||
cleanups.push(() => {
|
const clipboard = navigator.clipboard
|
||||||
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
|
if (clipboard?.writeText) {
|
||||||
|
clipboard.writeText(selection).catch(() => {})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
t.attachCustomKeyEventHandler((event) => {
|
||||||
|
const key = event.key.toLowerCase()
|
||||||
|
|
||||||
|
if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") {
|
||||||
|
copy()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.metaKey && !event.ctrlKey && !event.altKey && key === "c") {
|
||||||
|
if (!t.hasSelection()) return true
|
||||||
|
copy()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow for ctrl-` to toggle terminal in parent
|
||||||
|
if (event.ctrlKey && key === "`") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
fitAddon = new mod.FitAddon()
|
||||||
|
serializeAddon = new SerializeAddon()
|
||||||
|
t.loadAddon(serializeAddon)
|
||||||
|
t.loadAddon(fitAddon)
|
||||||
|
|
||||||
|
t.open(container)
|
||||||
|
container.addEventListener("pointerdown", handlePointerDown)
|
||||||
|
|
||||||
|
handleTextareaFocus = () => {
|
||||||
|
t.options.cursorBlink = true
|
||||||
|
}
|
||||||
|
handleTextareaBlur = () => {
|
||||||
|
t.options.cursorBlink = false
|
||||||
|
}
|
||||||
|
|
||||||
|
t.textarea?.addEventListener("focus", handleTextareaFocus)
|
||||||
|
t.textarea?.addEventListener("blur", handleTextareaBlur)
|
||||||
|
|
||||||
|
focusTerminal()
|
||||||
|
|
||||||
|
if (local.pty.buffer) {
|
||||||
|
if (local.pty.rows && local.pty.cols) {
|
||||||
|
t.resize(local.pty.cols, local.pty.rows)
|
||||||
|
}
|
||||||
|
t.write(local.pty.buffer, () => {
|
||||||
|
if (local.pty.scrollY) {
|
||||||
|
t.scrollToLine(local.pty.scrollY)
|
||||||
|
}
|
||||||
|
fitAddon.fit()
|
||||||
})
|
})
|
||||||
if (disposed) {
|
}
|
||||||
cleanup()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ws = socket
|
|
||||||
|
|
||||||
const t = new mod.Terminal({
|
fitAddon.observeResize()
|
||||||
cursorBlink: true,
|
handleResize = () => fitAddon.fit()
|
||||||
cursorStyle: "bar",
|
window.addEventListener("resize", handleResize)
|
||||||
fontSize: 14,
|
t.onResize(async (size) => {
|
||||||
fontFamily: monoFontFamily(settings.appearance.font()),
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
allowTransparency: true,
|
await sdk.client.pty
|
||||||
theme: terminalColors(),
|
|
||||||
scrollback: 10_000,
|
|
||||||
ghostty: g,
|
|
||||||
})
|
|
||||||
cleanups.push(() => t.dispose())
|
|
||||||
if (disposed) {
|
|
||||||
cleanup()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ghostty = g
|
|
||||||
term = t
|
|
||||||
|
|
||||||
const copy = () => {
|
|
||||||
const selection = t.getSelection()
|
|
||||||
if (!selection) return false
|
|
||||||
|
|
||||||
const body = document.body
|
|
||||||
if (body) {
|
|
||||||
const textarea = document.createElement("textarea")
|
|
||||||
textarea.value = selection
|
|
||||||
textarea.setAttribute("readonly", "")
|
|
||||||
textarea.style.position = "fixed"
|
|
||||||
textarea.style.opacity = "0"
|
|
||||||
body.appendChild(textarea)
|
|
||||||
textarea.select()
|
|
||||||
const copied = document.execCommand("copy")
|
|
||||||
body.removeChild(textarea)
|
|
||||||
if (copied) return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const clipboard = navigator.clipboard
|
|
||||||
if (clipboard?.writeText) {
|
|
||||||
clipboard.writeText(selection).catch(() => {})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
t.attachCustomKeyEventHandler((event) => {
|
|
||||||
const key = event.key.toLowerCase()
|
|
||||||
|
|
||||||
if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") {
|
|
||||||
copy()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.metaKey && !event.ctrlKey && !event.altKey && key === "c") {
|
|
||||||
if (!t.hasSelection()) return true
|
|
||||||
copy()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// allow for ctrl-` to toggle terminal in parent
|
|
||||||
if (event.ctrlKey && key === "`") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
const fit = new mod.FitAddon()
|
|
||||||
const serializer = new SerializeAddon()
|
|
||||||
cleanups.push(() => (fit as unknown as { dispose?: VoidFunction }).dispose?.())
|
|
||||||
t.loadAddon(serializer)
|
|
||||||
t.loadAddon(fit)
|
|
||||||
fitAddon = fit
|
|
||||||
serializeAddon = serializer
|
|
||||||
|
|
||||||
t.open(container)
|
|
||||||
container.addEventListener("pointerdown", handlePointerDown)
|
|
||||||
cleanups.push(() => container.removeEventListener("pointerdown", handlePointerDown))
|
|
||||||
|
|
||||||
handleTextareaFocus = () => {
|
|
||||||
t.options.cursorBlink = true
|
|
||||||
}
|
|
||||||
handleTextareaBlur = () => {
|
|
||||||
t.options.cursorBlink = false
|
|
||||||
}
|
|
||||||
|
|
||||||
t.textarea?.addEventListener("focus", handleTextareaFocus)
|
|
||||||
t.textarea?.addEventListener("blur", handleTextareaBlur)
|
|
||||||
cleanups.push(() => t.textarea?.removeEventListener("focus", handleTextareaFocus))
|
|
||||||
cleanups.push(() => t.textarea?.removeEventListener("blur", handleTextareaBlur))
|
|
||||||
|
|
||||||
focusTerminal()
|
|
||||||
|
|
||||||
if (local.pty.buffer) {
|
|
||||||
if (local.pty.rows && local.pty.cols) {
|
|
||||||
t.resize(local.pty.cols, local.pty.rows)
|
|
||||||
}
|
|
||||||
t.write(local.pty.buffer, () => {
|
|
||||||
if (local.pty.scrollY) {
|
|
||||||
t.scrollToLine(local.pty.scrollY)
|
|
||||||
}
|
|
||||||
fitAddon.fit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fit.observeResize()
|
|
||||||
handleResize = () => fit.fit()
|
|
||||||
window.addEventListener("resize", handleResize)
|
|
||||||
cleanups.push(() => window.removeEventListener("resize", handleResize))
|
|
||||||
const onResize = t.onResize(async (size) => {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
await sdk.client.pty
|
|
||||||
.update({
|
|
||||||
ptyID: local.pty.id,
|
|
||||||
size: {
|
|
||||||
cols: size.cols,
|
|
||||||
rows: size.rows,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
cleanups.push(() => (onResize as unknown as { dispose?: VoidFunction }).dispose?.())
|
|
||||||
const onData = t.onData((data) => {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
socket.send(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
cleanups.push(() => (onData as unknown as { dispose?: VoidFunction }).dispose?.())
|
|
||||||
const onKey = t.onKey((key) => {
|
|
||||||
if (key.key == "Enter") {
|
|
||||||
props.onSubmit?.()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
cleanups.push(() => (onKey as unknown as { dispose?: VoidFunction }).dispose?.())
|
|
||||||
// t.onScroll((ydisp) => {
|
|
||||||
// console.log("Scroll position:", ydisp)
|
|
||||||
// })
|
|
||||||
|
|
||||||
const handleOpen = () => {
|
|
||||||
local.onConnect?.()
|
|
||||||
sdk.client.pty
|
|
||||||
.update({
|
.update({
|
||||||
ptyID: local.pty.id,
|
ptyID: local.pty.id,
|
||||||
size: {
|
size: {
|
||||||
cols: t.cols,
|
cols: size.cols,
|
||||||
rows: t.rows,
|
rows: size.rows,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
socket.addEventListener("open", handleOpen)
|
})
|
||||||
cleanups.push(() => socket.removeEventListener("open", handleOpen))
|
t.onData((data) => {
|
||||||
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
const handleMessage = (event: MessageEvent) => {
|
socket.send(data)
|
||||||
t.write(event.data)
|
|
||||||
}
|
}
|
||||||
socket.addEventListener("message", handleMessage)
|
})
|
||||||
cleanups.push(() => socket.removeEventListener("message", handleMessage))
|
t.onKey((key) => {
|
||||||
|
if (key.key == "Enter") {
|
||||||
const handleError = (error: Event) => {
|
props.onSubmit?.()
|
||||||
if (disposed) return
|
}
|
||||||
|
})
|
||||||
|
// t.onScroll((ydisp) => {
|
||||||
|
// console.log("Scroll position:", ydisp)
|
||||||
|
// })
|
||||||
|
socket.addEventListener("open", () => {
|
||||||
|
local.onConnect?.()
|
||||||
|
sdk.client.pty
|
||||||
|
.update({
|
||||||
|
ptyID: local.pty.id,
|
||||||
|
size: {
|
||||||
|
cols: t.cols,
|
||||||
|
rows: t.rows,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
})
|
||||||
|
socket.addEventListener("message", (event) => {
|
||||||
|
t.write(event.data)
|
||||||
|
})
|
||||||
|
socket.addEventListener("error", (error) => {
|
||||||
|
if (disposed) return
|
||||||
|
if (once.value) return
|
||||||
|
once.value = true
|
||||||
|
console.error("WebSocket error:", error)
|
||||||
|
local.onConnectError?.(error)
|
||||||
|
})
|
||||||
|
socket.addEventListener("close", (event) => {
|
||||||
|
if (disposed) return
|
||||||
|
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
|
||||||
|
// For other codes (network issues, server restart), trigger error handler
|
||||||
|
if (event.code !== 1000) {
|
||||||
if (once.value) return
|
if (once.value) return
|
||||||
once.value = true
|
once.value = true
|
||||||
console.error("WebSocket error:", error)
|
local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
|
||||||
local.onConnectError?.(error)
|
|
||||||
}
|
}
|
||||||
socket.addEventListener("error", handleError)
|
|
||||||
cleanups.push(() => socket.removeEventListener("error", handleError))
|
|
||||||
|
|
||||||
const handleClose = (event: CloseEvent) => {
|
|
||||||
if (disposed) return
|
|
||||||
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
|
|
||||||
// For other codes (network issues, server restart), trigger error handler
|
|
||||||
if (event.code !== 1000) {
|
|
||||||
if (once.value) return
|
|
||||||
once.value = true
|
|
||||||
local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
socket.addEventListener("close", handleClose)
|
|
||||||
cleanups.push(() => socket.removeEventListener("close", handleClose))
|
|
||||||
}
|
|
||||||
|
|
||||||
void run().catch((err) => {
|
|
||||||
if (disposed) return
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("terminal.connectionLost.title"),
|
|
||||||
description: err instanceof Error ? err.message : language.t("terminal.connectionLost.description"),
|
|
||||||
})
|
|
||||||
local.onConnectError?.(err)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
disposed = true
|
disposed = true
|
||||||
|
if (handleResize) {
|
||||||
|
window.removeEventListener("resize", handleResize)
|
||||||
|
}
|
||||||
|
container.removeEventListener("pointerdown", handlePointerDown)
|
||||||
|
term?.textarea?.removeEventListener("focus", handleTextareaFocus)
|
||||||
|
term?.textarea?.removeEventListener("blur", handleTextareaBlur)
|
||||||
|
|
||||||
const t = term
|
const t = term
|
||||||
if (serializeAddon && props.onCleanup && t) {
|
if (serializeAddon && props.onCleanup && t) {
|
||||||
const buffer = (() => {
|
const buffer = serializeAddon.serialize()
|
||||||
try {
|
|
||||||
return serializeAddon.serialize()
|
|
||||||
} catch {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
props.onCleanup({
|
props.onCleanup({
|
||||||
...local.pty,
|
...local.pty,
|
||||||
buffer,
|
buffer,
|
||||||
@@ -375,7 +298,8 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup()
|
ws?.close()
|
||||||
|
t?.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { createEffect, createMemo, Show, untrack } from "solid-js"
|
import { createEffect, createMemo, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { useLocation, useNavigate } from "@solidjs/router"
|
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { useTheme } from "@opencode-ai/ui/theme"
|
import { useTheme } from "@opencode-ai/ui/theme"
|
||||||
|
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
@@ -18,68 +16,11 @@ export function Titlebar() {
|
|||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
||||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||||
const web = createMemo(() => platform.platform === "web")
|
const web = createMemo(() => platform.platform === "web")
|
||||||
|
|
||||||
const [history, setHistory] = createStore({
|
|
||||||
stack: [] as string[],
|
|
||||||
index: 0,
|
|
||||||
action: undefined as "back" | "forward" | undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const path = () => `${location.pathname}${location.search}${location.hash}`
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const current = path()
|
|
||||||
|
|
||||||
untrack(() => {
|
|
||||||
if (!history.stack.length) {
|
|
||||||
const stack = current === "/" ? ["/"] : ["/", current]
|
|
||||||
setHistory({ stack, index: stack.length - 1 })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const active = history.stack[history.index]
|
|
||||||
if (current === active) {
|
|
||||||
if (history.action) setHistory("action", undefined)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (history.action) {
|
|
||||||
setHistory("action", undefined)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = history.stack.slice(0, history.index + 1).concat(current)
|
|
||||||
setHistory({ stack: next, index: next.length - 1 })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const canBack = createMemo(() => history.index > 0)
|
|
||||||
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
|
||||||
|
|
||||||
const back = () => {
|
|
||||||
if (!canBack()) return
|
|
||||||
const index = history.index - 1
|
|
||||||
const to = history.stack[index]
|
|
||||||
if (!to) return
|
|
||||||
setHistory({ index, action: "back" })
|
|
||||||
navigate(to)
|
|
||||||
}
|
|
||||||
|
|
||||||
const forward = () => {
|
|
||||||
if (!canForward()) return
|
|
||||||
const index = history.index + 1
|
|
||||||
const to = history.stack[index]
|
|
||||||
if (!to) return
|
|
||||||
setHistory({ index, action: "forward" })
|
|
||||||
navigate(to)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getWin = () => {
|
const getWin = () => {
|
||||||
if (platform.platform !== "desktop") return
|
if (platform.platform !== "desktop") return
|
||||||
|
|
||||||
@@ -165,58 +106,34 @@ export function Titlebar() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-3 shrink-0">
|
<TooltipKeybind
|
||||||
<TooltipKeybind
|
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
placement="bottom"
|
||||||
placement="bottom"
|
title={language.t("command.sidebar.toggle")}
|
||||||
title={language.t("command.sidebar.toggle")}
|
keybind={command.keybind("sidebar.toggle")}
|
||||||
keybind={command.keybind("sidebar.toggle")}
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="group/sidebar-toggle size-6 p-0"
|
||||||
|
onClick={layout.sidebar.toggle}
|
||||||
|
aria-label={language.t("command.sidebar.toggle")}
|
||||||
|
aria-expanded={layout.sidebar.opened()}
|
||||||
>
|
>
|
||||||
<Button
|
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||||
variant="ghost"
|
<Icon
|
||||||
class="group/sidebar-toggle size-6 p-0"
|
size="small"
|
||||||
onClick={layout.sidebar.toggle}
|
name={layout.sidebar.opened() ? "layout-left-full" : "layout-left"}
|
||||||
aria-label={language.t("command.sidebar.toggle")}
|
class="group-hover/sidebar-toggle:hidden"
|
||||||
aria-expanded={layout.sidebar.opened()}
|
|
||||||
>
|
|
||||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name={layout.sidebar.opened() ? "layout-left-full" : "layout-left"}
|
|
||||||
class="group-hover/sidebar-toggle:hidden"
|
|
||||||
/>
|
|
||||||
<Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
|
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name={layout.sidebar.opened() ? "layout-left" : "layout-left-full"}
|
|
||||||
class="hidden group-active/sidebar-toggle:inline-block"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
<div class="hidden xl:flex items-center gap-1 shrink-0">
|
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
icon="arrow-left"
|
|
||||||
class="size-6 p-0"
|
|
||||||
disabled={!canBack()}
|
|
||||||
onClick={back}
|
|
||||||
aria-label={language.t("common.goBack")}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
<Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
<Icon
|
||||||
<Button
|
size="small"
|
||||||
variant="ghost"
|
name={layout.sidebar.opened() ? "layout-left" : "layout-left-full"}
|
||||||
icon="arrow-right"
|
class="hidden group-active/sidebar-toggle:inline-block"
|
||||||
class="size-6 p-0"
|
|
||||||
disabled={!canForward()}
|
|
||||||
onClick={forward}
|
|
||||||
aria-label={language.t("common.goForward")}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</TooltipKeybind>
|
||||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
|
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
|
||||||
<div class="flex-1 h-full" data-tauri-drag-region />
|
<div class="flex-1 h-full" data-tauri-drag-region />
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -151,28 +151,6 @@ const WORKSPACE_KEY = "__workspace__"
|
|||||||
const MAX_FILE_VIEW_SESSIONS = 20
|
const MAX_FILE_VIEW_SESSIONS = 20
|
||||||
const MAX_VIEW_FILES = 500
|
const MAX_VIEW_FILES = 500
|
||||||
|
|
||||||
const MAX_FILE_CONTENT_ENTRIES = 40
|
|
||||||
const MAX_FILE_CONTENT_BYTES = 20 * 1024 * 1024
|
|
||||||
|
|
||||||
const contentLru = new Map<string, number>()
|
|
||||||
|
|
||||||
function approxBytes(content: FileContent) {
|
|
||||||
const patchBytes =
|
|
||||||
content.patch?.hunks.reduce((total, hunk) => {
|
|
||||||
return total + hunk.lines.reduce((sum, line) => sum + line.length, 0)
|
|
||||||
}, 0) ?? 0
|
|
||||||
|
|
||||||
return (content.content.length + (content.diff?.length ?? 0) + patchBytes) * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
function touchContent(path: string, bytes?: number) {
|
|
||||||
const prev = contentLru.get(path)
|
|
||||||
if (prev === undefined && bytes === undefined) return
|
|
||||||
const value = bytes ?? prev ?? 0
|
|
||||||
contentLru.delete(path)
|
|
||||||
contentLru.set(path, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ViewSession = ReturnType<typeof createViewSession>
|
type ViewSession = ReturnType<typeof createViewSession>
|
||||||
|
|
||||||
type ViewCacheEntry = {
|
type ViewCacheEntry = {
|
||||||
@@ -317,12 +295,6 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
const inflight = new Map<string, Promise<void>>()
|
const inflight = new Map<string, Promise<void>>()
|
||||||
const treeInflight = new Map<string, Promise<void>>()
|
const treeInflight = new Map<string, Promise<void>>()
|
||||||
|
|
||||||
const search = (query: string, dirs: "true" | "false") =>
|
|
||||||
sdk.client.find.files({ query, dirs }).then(
|
|
||||||
(x) => (x.data ?? []).map(normalize),
|
|
||||||
() => [],
|
|
||||||
)
|
|
||||||
|
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
file: Record<string, FileState>
|
file: Record<string, FileState>
|
||||||
}>({
|
}>({
|
||||||
@@ -337,40 +309,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
dir: { "": { expanded: true } },
|
dir: { "": { expanded: true } },
|
||||||
})
|
})
|
||||||
|
|
||||||
const evictContent = (keep?: Set<string>) => {
|
|
||||||
const protectedSet = keep ?? new Set<string>()
|
|
||||||
const total = () => {
|
|
||||||
return Array.from(contentLru.values()).reduce((sum, bytes) => sum + bytes, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
while (contentLru.size > MAX_FILE_CONTENT_ENTRIES || total() > MAX_FILE_CONTENT_BYTES) {
|
|
||||||
const path = contentLru.keys().next().value
|
|
||||||
if (!path) return
|
|
||||||
|
|
||||||
if (protectedSet.has(path)) {
|
|
||||||
touchContent(path)
|
|
||||||
if (contentLru.size <= protectedSet.size) return
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
contentLru.delete(path)
|
|
||||||
if (!store.file[path]) continue
|
|
||||||
setStore(
|
|
||||||
"file",
|
|
||||||
path,
|
|
||||||
produce((draft) => {
|
|
||||||
draft.content = undefined
|
|
||||||
draft.loaded = false
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
scope()
|
scope()
|
||||||
inflight.clear()
|
inflight.clear()
|
||||||
treeInflight.clear()
|
treeInflight.clear()
|
||||||
contentLru.clear()
|
|
||||||
setStore("file", {})
|
setStore("file", {})
|
||||||
setTree("node", {})
|
setTree("node", {})
|
||||||
setTree("dir", { "": { expanded: true } })
|
setTree("dir", { "": { expanded: true } })
|
||||||
@@ -451,20 +393,15 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
.read({ path })
|
.read({ path })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
if (scope() !== directory) return
|
if (scope() !== directory) return
|
||||||
const content = x.data
|
|
||||||
setStore(
|
setStore(
|
||||||
"file",
|
"file",
|
||||||
path,
|
path,
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
draft.loaded = true
|
draft.loaded = true
|
||||||
draft.loading = false
|
draft.loading = false
|
||||||
draft.content = content
|
draft.content = x.data
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!content) return
|
|
||||||
touchContent(path, approxBytes(content))
|
|
||||||
evictContent(new Set([path]))
|
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (scope() !== directory) return
|
if (scope() !== directory) return
|
||||||
@@ -654,18 +591,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
listDir(parent, { force: true })
|
listDir(parent, { force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
const get = (input: string) => {
|
const get = (input: string) => store.file[normalize(input)]
|
||||||
const path = normalize(input)
|
|
||||||
const file = store.file[path]
|
|
||||||
const content = file?.content
|
|
||||||
if (!content) return file
|
|
||||||
if (contentLru.has(path)) {
|
|
||||||
touchContent(path)
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
touchContent(path, approxBytes(content))
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollTop = (input: string) => view().scrollTop(normalize(input))
|
const scrollTop = (input: string) => view().scrollTop(normalize(input))
|
||||||
const scrollLeft = (input: string) => view().scrollLeft(normalize(input))
|
const scrollLeft = (input: string) => view().scrollLeft(normalize(input))
|
||||||
@@ -719,8 +645,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
setScrollLeft,
|
setScrollLeft,
|
||||||
selectedLines,
|
selectedLines,
|
||||||
setSelectedLines,
|
setSelectedLines,
|
||||||
searchFiles: (query: string) => search(query, "false"),
|
searchFiles: (query: string) =>
|
||||||
searchFilesAndDirectories: (query: string) => search(query, "true"),
|
sdk.client.find.files({ query, dirs: "false" }).then((x) => (x.data ?? []).map(normalize)),
|
||||||
|
searchFilesAndDirectories: (query: string) =>
|
||||||
|
sdk.client.find.files({ query, dirs: "true" }).then((x) => (x.data ?? []).map(normalize)),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { createStore, produce, reconcile, type SetStoreFunction, type Store } fr
|
|||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { retry } from "@opencode-ai/util/retry"
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import type { InitError } from "../pages/error"
|
import { ErrorPage, type InitError } from "../pages/error"
|
||||||
import {
|
import {
|
||||||
batch,
|
batch,
|
||||||
createContext,
|
createContext,
|
||||||
@@ -188,74 +188,7 @@ function createGlobalSync() {
|
|||||||
config: {},
|
config: {},
|
||||||
reload: undefined,
|
reload: undefined,
|
||||||
})
|
})
|
||||||
|
let bootstrapQueue: string[] = []
|
||||||
const queued = new Set<string>()
|
|
||||||
let root = false
|
|
||||||
let running = false
|
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined
|
|
||||||
|
|
||||||
const paused = () => untrack(() => globalStore.reload) !== undefined
|
|
||||||
|
|
||||||
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
|
|
||||||
|
|
||||||
const take = (count: number) => {
|
|
||||||
if (queued.size === 0) return [] as string[]
|
|
||||||
const items: string[] = []
|
|
||||||
for (const item of queued) {
|
|
||||||
queued.delete(item)
|
|
||||||
items.push(item)
|
|
||||||
if (items.length >= count) break
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
const schedule = () => {
|
|
||||||
if (timer) return
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
timer = undefined
|
|
||||||
void drain()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const push = (directory: string) => {
|
|
||||||
if (!directory) return
|
|
||||||
queued.add(directory)
|
|
||||||
if (paused()) return
|
|
||||||
schedule()
|
|
||||||
}
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
root = true
|
|
||||||
if (paused()) return
|
|
||||||
schedule()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function drain() {
|
|
||||||
if (running) return
|
|
||||||
running = true
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
if (paused()) return
|
|
||||||
|
|
||||||
if (root) {
|
|
||||||
root = false
|
|
||||||
await bootstrap()
|
|
||||||
await tick()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirs = take(2)
|
|
||||||
if (dirs.length === 0) return
|
|
||||||
|
|
||||||
await Promise.all(dirs.map((dir) => bootstrapInstance(dir)))
|
|
||||||
await tick()
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
running = false
|
|
||||||
if (paused()) return
|
|
||||||
if (root || queued.size) schedule()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!projectCacheReady()) return
|
if (!projectCacheReady()) return
|
||||||
@@ -277,8 +210,14 @@ function createGlobalSync() {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (globalStore.reload !== "complete") return
|
if (globalStore.reload !== "complete") return
|
||||||
|
if (bootstrapQueue.length) {
|
||||||
|
for (const directory of bootstrapQueue) {
|
||||||
|
bootstrapInstance(directory)
|
||||||
|
}
|
||||||
|
bootstrap()
|
||||||
|
}
|
||||||
|
bootstrapQueue = []
|
||||||
setGlobalStore("reload", undefined)
|
setGlobalStore("reload", undefined)
|
||||||
refresh()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||||
@@ -607,37 +546,6 @@ function createGlobalSync() {
|
|||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
function purgeMessageParts(setStore: SetStoreFunction<State>, messageID: string | undefined) {
|
|
||||||
if (!messageID) return
|
|
||||||
setStore(
|
|
||||||
produce((draft) => {
|
|
||||||
delete draft.part[messageID]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function purgeSessionData(store: Store<State>, setStore: SetStoreFunction<State>, sessionID: string | undefined) {
|
|
||||||
if (!sessionID) return
|
|
||||||
|
|
||||||
const messages = store.message[sessionID]
|
|
||||||
const messageIDs = (messages ?? []).map((m) => m.id).filter((id): id is string => !!id)
|
|
||||||
|
|
||||||
setStore(
|
|
||||||
produce((draft) => {
|
|
||||||
delete draft.message[sessionID]
|
|
||||||
delete draft.session_diff[sessionID]
|
|
||||||
delete draft.todo[sessionID]
|
|
||||||
delete draft.permission[sessionID]
|
|
||||||
delete draft.question[sessionID]
|
|
||||||
delete draft.session_status[sessionID]
|
|
||||||
|
|
||||||
for (const messageID of messageIDs) {
|
|
||||||
delete draft.part[messageID]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsub = globalSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
const directory = e.name
|
const directory = e.name
|
||||||
const event = e.details
|
const event = e.details
|
||||||
@@ -645,8 +553,9 @@ function createGlobalSync() {
|
|||||||
if (directory === "global") {
|
if (directory === "global") {
|
||||||
switch (event?.type) {
|
switch (event?.type) {
|
||||||
case "global.disposed": {
|
case "global.disposed": {
|
||||||
refresh()
|
if (globalStore.reload) return
|
||||||
return
|
bootstrap()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
case "project.updated": {
|
case "project.updated": {
|
||||||
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
|
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
|
||||||
@@ -670,45 +579,14 @@ function createGlobalSync() {
|
|||||||
if (!existing) return
|
if (!existing) return
|
||||||
|
|
||||||
const [store, setStore] = existing
|
const [store, setStore] = existing
|
||||||
|
|
||||||
const cleanupSessionCaches = (sessionID: string) => {
|
|
||||||
if (!sessionID) return
|
|
||||||
|
|
||||||
const hasAny =
|
|
||||||
store.message[sessionID] !== undefined ||
|
|
||||||
store.session_diff[sessionID] !== undefined ||
|
|
||||||
store.todo[sessionID] !== undefined ||
|
|
||||||
store.permission[sessionID] !== undefined ||
|
|
||||||
store.question[sessionID] !== undefined ||
|
|
||||||
store.session_status[sessionID] !== undefined
|
|
||||||
|
|
||||||
if (!hasAny) return
|
|
||||||
|
|
||||||
setStore(
|
|
||||||
produce((draft) => {
|
|
||||||
const messages = draft.message[sessionID]
|
|
||||||
if (messages) {
|
|
||||||
for (const message of messages) {
|
|
||||||
const id = message?.id
|
|
||||||
if (!id) continue
|
|
||||||
delete draft.part[id]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delete draft.message[sessionID]
|
|
||||||
delete draft.session_diff[sessionID]
|
|
||||||
delete draft.todo[sessionID]
|
|
||||||
delete draft.permission[sessionID]
|
|
||||||
delete draft.question[sessionID]
|
|
||||||
delete draft.session_status[sessionID]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "server.instance.disposed": {
|
case "server.instance.disposed": {
|
||||||
push(directory)
|
if (globalStore.reload) {
|
||||||
return
|
bootstrapQueue.push(directory)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bootstrapInstance(directory)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
case "session.created": {
|
case "session.created": {
|
||||||
const info = event.properties.info
|
const info = event.properties.info
|
||||||
@@ -738,7 +616,6 @@ function createGlobalSync() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cleanupSessionCaches(info.id)
|
|
||||||
if (info.parentID) break
|
if (info.parentID) break
|
||||||
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||||
break
|
break
|
||||||
@@ -754,8 +631,7 @@ function createGlobalSync() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session.deleted": {
|
case "session.deleted": {
|
||||||
const sessionID = event.properties.info.id
|
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||||
const result = Binary.search(store.session, sessionID, (s) => s.id)
|
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
setStore(
|
setStore(
|
||||||
"session",
|
"session",
|
||||||
@@ -764,7 +640,6 @@ function createGlobalSync() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cleanupSessionCaches(sessionID)
|
|
||||||
if (event.properties.info.parentID) break
|
if (event.properties.info.parentID) break
|
||||||
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||||
break
|
break
|
||||||
@@ -800,22 +675,18 @@ function createGlobalSync() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "message.removed": {
|
case "message.removed": {
|
||||||
const sessionID = event.properties.sessionID
|
const messages = store.message[event.properties.sessionID]
|
||||||
const messageID = event.properties.messageID
|
if (!messages) break
|
||||||
|
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
|
||||||
setStore(
|
if (result.found) {
|
||||||
produce((draft) => {
|
setStore(
|
||||||
const messages = draft.message[sessionID]
|
"message",
|
||||||
if (messages) {
|
event.properties.sessionID,
|
||||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
produce((draft) => {
|
||||||
if (result.found) {
|
draft.splice(result.index, 1)
|
||||||
messages.splice(result.index, 1)
|
}),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete draft.part[messageID]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "message.part.updated": {
|
case "message.part.updated": {
|
||||||
@@ -840,19 +711,15 @@ function createGlobalSync() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "message.part.removed": {
|
case "message.part.removed": {
|
||||||
const messageID = event.properties.messageID
|
const parts = store.part[event.properties.messageID]
|
||||||
const parts = store.part[messageID]
|
|
||||||
if (!parts) break
|
if (!parts) break
|
||||||
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
|
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
setStore(
|
setStore(
|
||||||
|
"part",
|
||||||
|
event.properties.messageID,
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const list = draft.part[messageID]
|
draft.splice(result.index, 1)
|
||||||
if (!list) return
|
|
||||||
const next = Binary.search(list, event.properties.partID, (p) => p.id)
|
|
||||||
if (!next.found) return
|
|
||||||
list.splice(next.index, 1)
|
|
||||||
if (list.length === 0) delete draft.part[messageID]
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -949,10 +816,6 @@ function createGlobalSync() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
onCleanup(unsub)
|
onCleanup(unsub)
|
||||||
onCleanup(() => {
|
|
||||||
if (!timer) return
|
|
||||||
clearTimeout(timer)
|
|
||||||
})
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const health = await globalSDK.client.global
|
const health = await globalSDK.client.global
|
||||||
@@ -960,23 +823,18 @@ function createGlobalSync() {
|
|||||||
.then((x) => x.data)
|
.then((x) => x.data)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (!health?.healthy) {
|
if (!health?.healthy) {
|
||||||
showToast({
|
setGlobalStore("error", new Error(language.t("error.globalSync.connectFailed", { url: globalSDK.url })))
|
||||||
variant: "error",
|
|
||||||
title: language.t("dialog.server.add.error"),
|
|
||||||
description: language.t("error.globalSync.connectFailed", { url: globalSDK.url }),
|
|
||||||
})
|
|
||||||
setGlobalStore("ready", true)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = [
|
return Promise.all([
|
||||||
retry(() =>
|
retry(() =>
|
||||||
globalSDK.client.path.get().then((x) => {
|
globalSDK.client.path.get().then((x) => {
|
||||||
setGlobalStore("path", x.data!)
|
setGlobalStore("path", x.data!)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
retry(() =>
|
retry(() =>
|
||||||
globalSDK.client.global.config.get().then((x) => {
|
globalSDK.client.config.get().then((x) => {
|
||||||
setGlobalStore("config", x.data!)
|
setGlobalStore("config", x.data!)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -1000,22 +858,9 @@ function createGlobalSync() {
|
|||||||
setGlobalStore("provider_auth", x.data ?? {})
|
setGlobalStore("provider_auth", x.data ?? {})
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
]
|
])
|
||||||
|
.then(() => setGlobalStore("ready", true))
|
||||||
const results = await Promise.allSettled(tasks)
|
.catch((e) => setGlobalStore("error", e))
|
||||||
const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
const message = errors[0] instanceof Error ? errors[0].message : String(errors[0])
|
|
||||||
const more = errors.length > 1 ? ` (+${errors.length - 1} more)` : ""
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: message + more,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setGlobalStore("ready", true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -1059,13 +904,13 @@ function createGlobalSync() {
|
|||||||
},
|
},
|
||||||
child,
|
child,
|
||||||
bootstrap,
|
bootstrap,
|
||||||
updateConfig: (config: Config) => {
|
updateConfig: async (config: Config) => {
|
||||||
setGlobalStore("reload", "pending")
|
setGlobalStore("reload", "pending")
|
||||||
return globalSDK.client.global.config.update({ config }).finally(() => {
|
const response = await globalSDK.client.config.update({ config })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setGlobalStore("reload", "complete")
|
setGlobalStore("reload", "complete")
|
||||||
}, 1000)
|
}, 1000)
|
||||||
})
|
return response
|
||||||
},
|
},
|
||||||
project: {
|
project: {
|
||||||
loadSessions,
|
loadSessions,
|
||||||
@@ -1081,6 +926,9 @@ export function GlobalSyncProvider(props: ParentProps) {
|
|||||||
const value = createGlobalSync()
|
const value = createGlobalSync()
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Match when={value.error}>
|
||||||
|
<ErrorPage error={value.error} />
|
||||||
|
</Match>
|
||||||
<Match when={value.ready}>
|
<Match when={value.ready}>
|
||||||
<GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
|
<GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; p
|
|||||||
seen.add(key)
|
seen.add(key)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
return unique.slice(0, 5)
|
return unique.slice(0, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({
|
export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { dict as ru } from "@/i18n/ru"
|
|||||||
import { dict as ar } from "@/i18n/ar"
|
import { dict as ar } from "@/i18n/ar"
|
||||||
import { dict as no } from "@/i18n/no"
|
import { dict as no } from "@/i18n/no"
|
||||||
import { dict as br } from "@/i18n/br"
|
import { dict as br } from "@/i18n/br"
|
||||||
import { dict as th } from "@/i18n/th"
|
|
||||||
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||||
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
|
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
|
||||||
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
|
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
|
||||||
@@ -32,45 +31,13 @@ import { dict as uiRu } from "@opencode-ai/ui/i18n/ru"
|
|||||||
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
|
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
|
||||||
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
|
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
|
||||||
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
|
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
|
||||||
import { dict as uiTh } from "@opencode-ai/ui/i18n/th"
|
|
||||||
|
|
||||||
export type Locale =
|
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
|
||||||
| "en"
|
|
||||||
| "zh"
|
|
||||||
| "zht"
|
|
||||||
| "ko"
|
|
||||||
| "de"
|
|
||||||
| "es"
|
|
||||||
| "fr"
|
|
||||||
| "da"
|
|
||||||
| "ja"
|
|
||||||
| "pl"
|
|
||||||
| "ru"
|
|
||||||
| "ar"
|
|
||||||
| "no"
|
|
||||||
| "br"
|
|
||||||
| "th"
|
|
||||||
|
|
||||||
type RawDictionary = typeof en & typeof uiEn
|
type RawDictionary = typeof en & typeof uiEn
|
||||||
type Dictionary = i18n.Flatten<RawDictionary>
|
type Dictionary = i18n.Flatten<RawDictionary>
|
||||||
|
|
||||||
const LOCALES: readonly Locale[] = [
|
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
|
||||||
"en",
|
|
||||||
"zh",
|
|
||||||
"zht",
|
|
||||||
"ko",
|
|
||||||
"de",
|
|
||||||
"es",
|
|
||||||
"fr",
|
|
||||||
"da",
|
|
||||||
"ja",
|
|
||||||
"pl",
|
|
||||||
"ru",
|
|
||||||
"ar",
|
|
||||||
"no",
|
|
||||||
"br",
|
|
||||||
"th",
|
|
||||||
]
|
|
||||||
|
|
||||||
function detectLocale(): Locale {
|
function detectLocale(): Locale {
|
||||||
if (typeof navigator !== "object") return "en"
|
if (typeof navigator !== "object") return "en"
|
||||||
@@ -98,7 +65,6 @@ function detectLocale(): Locale {
|
|||||||
)
|
)
|
||||||
return "no"
|
return "no"
|
||||||
if (language.toLowerCase().startsWith("pt")) return "br"
|
if (language.toLowerCase().startsWith("pt")) return "br"
|
||||||
if (language.toLowerCase().startsWith("th")) return "th"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "en"
|
return "en"
|
||||||
@@ -128,7 +94,6 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
|||||||
if (store.locale === "ar") return "ar"
|
if (store.locale === "ar") return "ar"
|
||||||
if (store.locale === "no") return "no"
|
if (store.locale === "no") return "no"
|
||||||
if (store.locale === "br") return "br"
|
if (store.locale === "br") return "br"
|
||||||
if (store.locale === "th") return "th"
|
|
||||||
return "en"
|
return "en"
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -153,7 +118,6 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
|||||||
if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }
|
if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }
|
||||||
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
|
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
|
||||||
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
|
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
|
||||||
if (locale() === "th") return { ...base, ...i18n.flatten({ ...th, ...uiTh }) }
|
|
||||||
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
|
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -174,7 +138,6 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
|||||||
ar: "language.ar",
|
ar: "language.ar",
|
||||||
no: "language.no",
|
no: "language.no",
|
||||||
br: "language.br",
|
br: "language.br",
|
||||||
th: "language.th",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = (value: Locale) => t(labelKey[value])
|
const label = (value: Locale) => t(labelKey[value])
|
||||||
|
|||||||
@@ -51,37 +51,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
|
|
||||||
const migrate = (value: unknown) => {
|
const migrate = (value: unknown) => {
|
||||||
if (!isRecord(value)) return value
|
if (!isRecord(value)) return value
|
||||||
|
|
||||||
const sidebar = value.sidebar
|
const sidebar = value.sidebar
|
||||||
const migratedSidebar = (() => {
|
if (!isRecord(sidebar)) return value
|
||||||
if (!isRecord(sidebar)) return sidebar
|
if (typeof sidebar.workspaces !== "boolean") return value
|
||||||
if (typeof sidebar.workspaces !== "boolean") return sidebar
|
return {
|
||||||
return {
|
...value,
|
||||||
|
sidebar: {
|
||||||
...sidebar,
|
...sidebar,
|
||||||
workspaces: {},
|
workspaces: {},
|
||||||
workspacesDefault: sidebar.workspaces,
|
workspacesDefault: sidebar.workspaces,
|
||||||
}
|
},
|
||||||
})()
|
|
||||||
|
|
||||||
const fileTree = value.fileTree
|
|
||||||
const migratedFileTree = (() => {
|
|
||||||
if (!isRecord(fileTree)) return fileTree
|
|
||||||
if (fileTree.tab === "changes" || fileTree.tab === "all") return fileTree
|
|
||||||
|
|
||||||
const width = typeof fileTree.width === "number" ? fileTree.width : 344
|
|
||||||
return {
|
|
||||||
...fileTree,
|
|
||||||
opened: true,
|
|
||||||
width: width === 260 ? 344 : width,
|
|
||||||
tab: "changes",
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (migratedSidebar === sidebar && migratedFileTree === fileTree) return value
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
sidebar: migratedSidebar,
|
|
||||||
fileTree: migratedFileTree,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,11 +80,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
review: {
|
review: {
|
||||||
diffStyle: "split" as ReviewDiffStyle,
|
diffStyle: "split" as ReviewDiffStyle,
|
||||||
|
panelOpened: true,
|
||||||
},
|
},
|
||||||
fileTree: {
|
fileTree: {
|
||||||
opened: true,
|
opened: false,
|
||||||
width: 344,
|
width: 260,
|
||||||
tab: "changes" as "changes" | "all",
|
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
width: 600,
|
width: 600,
|
||||||
@@ -475,40 +454,32 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
fileTree: {
|
fileTree: {
|
||||||
opened: createMemo(() => store.fileTree?.opened ?? true),
|
opened: createMemo(() => store.fileTree?.opened ?? false),
|
||||||
width: createMemo(() => store.fileTree?.width ?? 344),
|
width: createMemo(() => store.fileTree?.width ?? 260),
|
||||||
tab: createMemo(() => store.fileTree?.tab ?? "changes"),
|
|
||||||
setTab(tab: "changes" | "all") {
|
|
||||||
if (!store.fileTree) {
|
|
||||||
setStore("fileTree", { opened: true, width: 344, tab })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setStore("fileTree", "tab", tab)
|
|
||||||
},
|
|
||||||
open() {
|
open() {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: true, width: 344, tab: "changes" })
|
setStore("fileTree", { opened: true, width: 260 })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "opened", true)
|
setStore("fileTree", "opened", true)
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: false, width: 344, tab: "changes" })
|
setStore("fileTree", { opened: false, width: 260 })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "opened", false)
|
setStore("fileTree", "opened", false)
|
||||||
},
|
},
|
||||||
toggle() {
|
toggle() {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: true, width: 344, tab: "changes" })
|
setStore("fileTree", { opened: true, width: 260 })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "opened", (x) => !x)
|
setStore("fileTree", "opened", (x) => !x)
|
||||||
},
|
},
|
||||||
resize(width: number) {
|
resize(width: number) {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: true, width, tab: "changes" })
|
setStore("fileTree", { opened: true, width })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "width", width)
|
setStore("fileTree", "width", width)
|
||||||
@@ -555,6 +526,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
|
|
||||||
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
||||||
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
||||||
|
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
|
||||||
|
|
||||||
function setTerminalOpened(next: boolean) {
|
function setTerminalOpened(next: boolean) {
|
||||||
const current = store.terminal
|
const current = store.terminal
|
||||||
@@ -568,6 +540,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
setStore("terminal", "opened", next)
|
setStore("terminal", "opened", next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setReviewPanelOpened(next: boolean) {
|
||||||
|
const current = store.review
|
||||||
|
if (!current) {
|
||||||
|
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = current.panelOpened ?? true
|
||||||
|
if (value === next) return
|
||||||
|
setStore("review", "panelOpened", next)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scroll(tab: string) {
|
scroll(tab: string) {
|
||||||
return scroll.scroll(key(), tab)
|
return scroll.scroll(key(), tab)
|
||||||
@@ -587,6 +571,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
setTerminalOpened(!terminalOpened())
|
setTerminalOpened(!terminalOpened())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
reviewPanel: {
|
||||||
|
opened: reviewPanelOpened,
|
||||||
|
open() {
|
||||||
|
setReviewPanelOpened(true)
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
setReviewPanelOpened(false)
|
||||||
|
},
|
||||||
|
toggle() {
|
||||||
|
setReviewPanelOpened(!reviewPanelOpened())
|
||||||
|
},
|
||||||
|
},
|
||||||
review: {
|
review: {
|
||||||
open: createMemo(() => s().reviewOpen),
|
open: createMemo(() => s().reviewOpen),
|
||||||
setOpen(open: string[]) {
|
setOpen(open: string[]) {
|
||||||
@@ -624,11 +620,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
||||||
return {
|
return {
|
||||||
tabs,
|
tabs,
|
||||||
active: createMemo(() => (tabs().active === "review" ? undefined : tabs().active)),
|
active: createMemo(() => tabs().active),
|
||||||
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
|
all: createMemo(() => tabs().all),
|
||||||
setActive(tab: string | undefined) {
|
setActive(tab: string | undefined) {
|
||||||
const session = key()
|
const session = key()
|
||||||
if (tab === "review") return
|
|
||||||
if (!store.sessionTabs[session]) {
|
if (!store.sessionTabs[session]) {
|
||||||
setStore("sessionTabs", session, { all: [], active: tab })
|
setStore("sessionTabs", session, { all: [], active: tab })
|
||||||
} else {
|
} else {
|
||||||
@@ -637,18 +632,25 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
setAll(all: string[]) {
|
setAll(all: string[]) {
|
||||||
const session = key()
|
const session = key()
|
||||||
const next = all.filter((tab) => tab !== "review")
|
|
||||||
if (!store.sessionTabs[session]) {
|
if (!store.sessionTabs[session]) {
|
||||||
setStore("sessionTabs", session, { all: next, active: undefined })
|
setStore("sessionTabs", session, { all, active: undefined })
|
||||||
} else {
|
} else {
|
||||||
setStore("sessionTabs", session, "all", next)
|
setStore("sessionTabs", session, "all", all)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async open(tab: string) {
|
async open(tab: string) {
|
||||||
if (tab === "review") return
|
|
||||||
const session = key()
|
const session = key()
|
||||||
const current = store.sessionTabs[session] ?? { all: [] }
|
const current = store.sessionTabs[session] ?? { all: [] }
|
||||||
|
|
||||||
|
if (tab === "review") {
|
||||||
|
if (!store.sessionTabs[session]) {
|
||||||
|
setStore("sessionTabs", session, { all: [], active: tab })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("sessionTabs", session, "active", tab)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (tab === "context") {
|
if (tab === "context") {
|
||||||
const all = [tab, ...current.all.filter((x) => x !== tab)]
|
const all = [tab, ...current.all.filter((x) => x !== tab)]
|
||||||
if (!store.sessionTabs[session]) {
|
if (!store.sessionTabs[session]) {
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import { usePlatform } from "@/context/platform"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
|
||||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { playSound, soundSrc } from "@/utils/sound"
|
import { playSound, soundSrc } from "@/utils/sound"
|
||||||
@@ -56,7 +55,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
const empty: Notification[] = []
|
const empty: Notification[] = []
|
||||||
|
|
||||||
const currentDirectory = createMemo(() => {
|
const currentDirectory = createMemo(() => {
|
||||||
return decode64(params.dir)
|
if (!params.dir) return
|
||||||
|
return base64Decode(params.dir)
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentSession = createMemo(() => params.id)
|
const currentSession = createMemo(() => params.id)
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { Persist, persisted } from "@/utils/persist"
|
|||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "./global-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
|
||||||
|
|
||||||
type PermissionRespondFn = (input: {
|
type PermissionRespondFn = (input: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
@@ -54,7 +53,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
|
|
||||||
const permissionsEnabled = createMemo(() => {
|
const permissionsEnabled = createMemo(() => {
|
||||||
const directory = decode64(params.dir)
|
const directory = params.dir ? base64Decode(params.dir) : undefined
|
||||||
if (!directory) return false
|
if (!directory) return false
|
||||||
const [store] = globalSync.child(directory)
|
const [store] = globalSync.child(directory)
|
||||||
return hasAutoAcceptPermissionConfig(store.config.permission)
|
return hasAutoAcceptPermissionConfig(store.config.permission)
|
||||||
@@ -67,21 +66,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const MAX_RESPONDED = 1000
|
const responded = new Set<string>()
|
||||||
const RESPONDED_TTL_MS = 60 * 60 * 1000
|
|
||||||
const responded = new Map<string, number>()
|
|
||||||
|
|
||||||
function pruneResponded(now: number) {
|
|
||||||
for (const [id, ts] of responded) {
|
|
||||||
if (now - ts < RESPONDED_TTL_MS) break
|
|
||||||
responded.delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const id of responded.keys()) {
|
|
||||||
if (responded.size <= MAX_RESPONDED) break
|
|
||||||
responded.delete(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const respond: PermissionRespondFn = (input) => {
|
const respond: PermissionRespondFn = (input) => {
|
||||||
globalSDK.client.permission.respond(input).catch(() => {
|
globalSDK.client.permission.respond(input).catch(() => {
|
||||||
@@ -90,12 +75,8 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
}
|
}
|
||||||
|
|
||||||
function respondOnce(permission: PermissionRequest, directory?: string) {
|
function respondOnce(permission: PermissionRequest, directory?: string) {
|
||||||
const now = Date.now()
|
if (responded.has(permission.id)) return
|
||||||
const hit = responded.has(permission.id)
|
responded.add(permission.id)
|
||||||
responded.delete(permission.id)
|
|
||||||
responded.set(permission.id, now)
|
|
||||||
pruneResponded(now)
|
|
||||||
if (hit) return
|
|
||||||
respond({
|
respond({
|
||||||
sessionID: permission.sessionID,
|
sessionID: permission.sessionID,
|
||||||
permissionID: permission.id,
|
permissionID: permission.id,
|
||||||
|
|||||||
@@ -17,12 +17,6 @@ export type Platform = {
|
|||||||
/** Restart the app */
|
/** Restart the app */
|
||||||
restart(): Promise<void>
|
restart(): Promise<void>
|
||||||
|
|
||||||
/** Navigate back in history */
|
|
||||||
back(): void
|
|
||||||
|
|
||||||
/** Navigate forward in history */
|
|
||||||
forward(): void
|
|
||||||
|
|
||||||
/** Send a system notification (optional deep link) */
|
/** Send a system notification (optional deep link) */
|
||||||
notify(title: string, description?: string, href?: string): Promise<void>
|
notify(title: string, description?: string, href?: string): Promise<void>
|
||||||
|
|
||||||
|
|||||||
@@ -95,11 +95,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
const isReady = createMemo(() => ready() && !!state.active)
|
const isReady = createMemo(() => ready() && !!state.active)
|
||||||
|
|
||||||
const check = (url: string) => {
|
const check = (url: string) => {
|
||||||
const signal = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(3000)
|
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: url,
|
baseUrl: url,
|
||||||
fetch: platform.fetch,
|
fetch: platform.fetch,
|
||||||
signal,
|
signal: AbortSignal.timeout(3000),
|
||||||
})
|
})
|
||||||
return sdk.global
|
return sdk.global
|
||||||
.health()
|
.health()
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ export interface Settings {
|
|||||||
autoSave: boolean
|
autoSave: boolean
|
||||||
releaseNotes: boolean
|
releaseNotes: boolean
|
||||||
}
|
}
|
||||||
updates: {
|
|
||||||
startup: boolean
|
|
||||||
}
|
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: number
|
fontSize: number
|
||||||
font: string
|
font: string
|
||||||
@@ -40,9 +37,6 @@ const defaultSettings: Settings = {
|
|||||||
autoSave: true,
|
autoSave: true,
|
||||||
releaseNotes: true,
|
releaseNotes: true,
|
||||||
},
|
},
|
||||||
updates: {
|
|
||||||
startup: true,
|
|
||||||
},
|
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
font: "ibm-plex-mono",
|
font: "ibm-plex-mono",
|
||||||
@@ -110,12 +104,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setStore("general", "releaseNotes", value)
|
setStore("general", "releaseNotes", value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updates: {
|
|
||||||
startup: createMemo(() => store.updates?.startup ?? defaultSettings.updates.startup),
|
|
||||||
setStartup(value: boolean) {
|
|
||||||
setStore("updates", "startup", value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
|
fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
|
||||||
setFontSize(value: number) {
|
setFontSize(value: number) {
|
||||||
|
|||||||
@@ -155,9 +155,8 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, sess
|
|||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setStore("all", index, {
|
setStore("all", index, {
|
||||||
id: clone.data.id,
|
...pty,
|
||||||
title: clone.data.title ?? pty.title,
|
...clone.data,
|
||||||
titleNumber: pty.titleNumber,
|
|
||||||
})
|
})
|
||||||
if (active) {
|
if (active) {
|
||||||
setStore("active", clone.data.id)
|
setStore("active", clone.data.id)
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ const platform: Platform = {
|
|||||||
openLink(url: string) {
|
openLink(url: string) {
|
||||||
window.open(url, "_blank")
|
window.open(url, "_blank")
|
||||||
},
|
},
|
||||||
back() {
|
|
||||||
window.history.back()
|
|
||||||
},
|
|
||||||
forward() {
|
|
||||||
window.history.forward()
|
|
||||||
},
|
|
||||||
restart: async () => {
|
restart: async () => {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ export const popularProviders = ["opencode", "anthropic", "github-copilot", "ope
|
|||||||
export function useProviders() {
|
export function useProviders() {
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const currentDirectory = createMemo(() => decode64(params.dir) ?? "")
|
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||||
const providers = createMemo(() => {
|
const providers = createMemo(() => {
|
||||||
if (currentDirectory()) {
|
if (currentDirectory()) {
|
||||||
const [projectStore] = globalSync.child(currentDirectory())
|
const [projectStore] = globalSync.child(currentDirectory())
|
||||||
|
|||||||
@@ -331,7 +331,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "لغة",
|
"toast.language.title": "لغة",
|
||||||
"toast.language.description": "تم التبديل إلى {{language}}",
|
"toast.language.description": "تم التبديل إلى {{language}}",
|
||||||
@@ -513,7 +512,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "المظهر",
|
"settings.general.section.appearance": "المظهر",
|
||||||
"settings.general.section.notifications": "إشعارات النظام",
|
"settings.general.section.notifications": "إشعارات النظام",
|
||||||
"settings.general.section.updates": "التحديثات",
|
|
||||||
"settings.general.section.sounds": "المؤثرات الصوتية",
|
"settings.general.section.sounds": "المؤثرات الصوتية",
|
||||||
|
|
||||||
"settings.general.row.language.title": "اللغة",
|
"settings.general.row.language.title": "اللغة",
|
||||||
@@ -524,18 +522,6 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
||||||
"settings.general.row.font.title": "الخط",
|
"settings.general.row.font.title": "الخط",
|
||||||
"settings.general.row.font.description": "تخصيص الخط الأحادي المستخدم في كتل التعليمات البرمجية",
|
"settings.general.row.font.description": "تخصيص الخط الأحادي المستخدم في كتل التعليمات البرمجية",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "ملاحظات الإصدار",
|
|
||||||
"settings.general.row.releaseNotes.description": 'عرض نوافذ "ما الجديد" المنبثقة بعد التحديثات',
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "التحقق من التحديثات عند بدء التشغيل",
|
|
||||||
"settings.updates.row.startup.description": "التحقق تلقائيًا من التحديثات عند تشغيل OpenCode",
|
|
||||||
"settings.updates.row.check.title": "التحقق من التحديثات",
|
|
||||||
"settings.updates.row.check.description": "التحقق يدويًا من التحديثات وتثبيتها إذا كانت متاحة",
|
|
||||||
"settings.updates.action.checkNow": "تحقق الآن",
|
|
||||||
"settings.updates.action.checking": "جارٍ التحقق...",
|
|
||||||
"settings.updates.toast.latest.title": "أنت على آخر إصدار",
|
|
||||||
"settings.updates.toast.latest.description": "أنت تستخدم أحدث إصدار من OpenCode.",
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -330,7 +330,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "Idioma",
|
"toast.language.title": "Idioma",
|
||||||
"toast.language.description": "Alterado para {{language}}",
|
"toast.language.description": "Alterado para {{language}}",
|
||||||
@@ -517,7 +516,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "Aparência",
|
"settings.general.section.appearance": "Aparência",
|
||||||
"settings.general.section.notifications": "Notificações do sistema",
|
"settings.general.section.notifications": "Notificações do sistema",
|
||||||
"settings.general.section.updates": "Atualizações",
|
|
||||||
"settings.general.section.sounds": "Efeitos sonoros",
|
"settings.general.section.sounds": "Efeitos sonoros",
|
||||||
|
|
||||||
"settings.general.row.language.title": "Idioma",
|
"settings.general.row.language.title": "Idioma",
|
||||||
@@ -528,18 +526,6 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
||||||
"settings.general.row.font.title": "Fonte",
|
"settings.general.row.font.title": "Fonte",
|
||||||
"settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código",
|
"settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "Notas da versão",
|
|
||||||
"settings.general.row.releaseNotes.description": 'Mostrar pop-ups de "Novidades" após atualizações',
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "Verificar atualizações ao iniciar",
|
|
||||||
"settings.updates.row.startup.description": "Verificar atualizações automaticamente quando o OpenCode iniciar",
|
|
||||||
"settings.updates.row.check.title": "Verificar atualizações",
|
|
||||||
"settings.updates.row.check.description": "Verificar atualizações manualmente e instalar se houver",
|
|
||||||
"settings.updates.action.checkNow": "Verificar agora",
|
|
||||||
"settings.updates.action.checking": "Verificando...",
|
|
||||||
"settings.updates.toast.latest.title": "Você está atualizado",
|
|
||||||
"settings.updates.toast.latest.description": "Você está usando a versão mais recente do OpenCode.",
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "Sprog",
|
"toast.language.title": "Sprog",
|
||||||
"toast.language.description": "Skiftede til {{language}}",
|
"toast.language.description": "Skiftede til {{language}}",
|
||||||
@@ -517,7 +516,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "Udseende",
|
"settings.general.section.appearance": "Udseende",
|
||||||
"settings.general.section.notifications": "Systemmeddelelser",
|
"settings.general.section.notifications": "Systemmeddelelser",
|
||||||
"settings.general.section.updates": "Opdateringer",
|
|
||||||
"settings.general.section.sounds": "Lydeffekter",
|
"settings.general.section.sounds": "Lydeffekter",
|
||||||
|
|
||||||
"settings.general.row.language.title": "Sprog",
|
"settings.general.row.language.title": "Sprog",
|
||||||
@@ -529,18 +527,6 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Skrifttype",
|
"settings.general.row.font.title": "Skrifttype",
|
||||||
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
|
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "Udgivelsesnoter",
|
|
||||||
"settings.general.row.releaseNotes.description": 'Vis "Hvad er nyt"-popups efter opdateringer',
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "Tjek for opdateringer ved opstart",
|
|
||||||
"settings.updates.row.startup.description": "Tjek automatisk for opdateringer, når OpenCode starter",
|
|
||||||
"settings.updates.row.check.title": "Tjek for opdateringer",
|
|
||||||
"settings.updates.row.check.description": "Tjek manuelt for opdateringer og installer, hvis tilgængelig",
|
|
||||||
"settings.updates.action.checkNow": "Tjek nu",
|
|
||||||
"settings.updates.action.checking": "Tjekker...",
|
|
||||||
"settings.updates.toast.latest.title": "Du er opdateret",
|
|
||||||
"settings.updates.toast.latest.description": "Du kører den nyeste version af OpenCode.",
|
|
||||||
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -338,7 +338,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "Sprache",
|
"toast.language.title": "Sprache",
|
||||||
"toast.language.description": "Zu {{language}} gewechselt",
|
"toast.language.description": "Zu {{language}} gewechselt",
|
||||||
@@ -527,7 +526,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "Erscheinungsbild",
|
"settings.general.section.appearance": "Erscheinungsbild",
|
||||||
"settings.general.section.notifications": "Systembenachrichtigungen",
|
"settings.general.section.notifications": "Systembenachrichtigungen",
|
||||||
"settings.general.section.updates": "Updates",
|
|
||||||
"settings.general.section.sounds": "Soundeffekte",
|
"settings.general.section.sounds": "Soundeffekte",
|
||||||
|
|
||||||
"settings.general.row.language.title": "Sprache",
|
"settings.general.row.language.title": "Sprache",
|
||||||
@@ -539,18 +537,6 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Schriftart",
|
"settings.general.row.font.title": "Schriftart",
|
||||||
"settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen",
|
"settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "Versionshinweise",
|
|
||||||
"settings.general.row.releaseNotes.description": '"Neuigkeiten"-Pop-ups nach Updates anzeigen',
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "Beim Start nach Updates suchen",
|
|
||||||
"settings.updates.row.startup.description": "Beim Start von OpenCode automatisch nach Updates suchen",
|
|
||||||
"settings.updates.row.check.title": "Nach Updates suchen",
|
|
||||||
"settings.updates.row.check.description": "Manuell nach Updates suchen und installieren, wenn verfügbar",
|
|
||||||
"settings.updates.action.checkNow": "Jetzt prüfen",
|
|
||||||
"settings.updates.action.checking": "Wird geprüft...",
|
|
||||||
"settings.updates.toast.latest.title": "Du bist auf dem neuesten Stand",
|
|
||||||
"settings.updates.toast.latest.description": "Du verwendest die aktuelle Version von OpenCode.",
|
|
||||||
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -166,8 +166,7 @@ export const dict = {
|
|||||||
"model.tooltip.context": "Context limit {{limit}}",
|
"model.tooltip.context": "Context limit {{limit}}",
|
||||||
|
|
||||||
"common.search.placeholder": "Search",
|
"common.search.placeholder": "Search",
|
||||||
"common.goBack": "Back",
|
"common.goBack": "Go back",
|
||||||
"common.goForward": "Forward",
|
|
||||||
"common.loading": "Loading",
|
"common.loading": "Loading",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Cancel",
|
"common.cancel": "Cancel",
|
||||||
@@ -337,7 +336,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "Language",
|
"toast.language.title": "Language",
|
||||||
"toast.language.description": "Switched to {{language}}",
|
"toast.language.description": "Switched to {{language}}",
|
||||||
@@ -541,15 +539,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "Release notes",
|
"settings.general.row.releaseNotes.title": "Release notes",
|
||||||
"settings.general.row.releaseNotes.description": "Show What's New popups after updates",
|
"settings.general.row.releaseNotes.description": "Show What's New popups after updates",
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "Check for updates on startup",
|
|
||||||
"settings.updates.row.startup.description": "Automatically check for updates when OpenCode launches",
|
|
||||||
"settings.updates.row.check.title": "Check for updates",
|
|
||||||
"settings.updates.row.check.description": "Manually check for updates and install if available",
|
|
||||||
"settings.updates.action.checkNow": "Check now",
|
|
||||||
"settings.updates.action.checking": "Checking...",
|
|
||||||
"settings.updates.toast.latest.title": "You're up to date",
|
|
||||||
"settings.updates.toast.latest.description": "You're running the latest version of OpenCode.",
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -333,7 +333,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "Idioma",
|
"toast.language.title": "Idioma",
|
||||||
"toast.language.description": "Cambiado a {{language}}",
|
"toast.language.description": "Cambiado a {{language}}",
|
||||||
@@ -520,7 +519,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "Apariencia",
|
"settings.general.section.appearance": "Apariencia",
|
||||||
"settings.general.section.notifications": "Notificaciones del sistema",
|
"settings.general.section.notifications": "Notificaciones del sistema",
|
||||||
"settings.general.section.updates": "Actualizaciones",
|
|
||||||
"settings.general.section.sounds": "Efectos de sonido",
|
"settings.general.section.sounds": "Efectos de sonido",
|
||||||
|
|
||||||
"settings.general.row.language.title": "Idioma",
|
"settings.general.row.language.title": "Idioma",
|
||||||
@@ -532,19 +530,6 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Fuente",
|
"settings.general.row.font.title": "Fuente",
|
||||||
"settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código",
|
"settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "Notas de la versión",
|
|
||||||
"settings.general.row.releaseNotes.description":
|
|
||||||
'Mostrar ventanas emergentes de "Novedades" después de las actualizaciones',
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "Buscar actualizaciones al iniciar",
|
|
||||||
"settings.updates.row.startup.description": "Buscar actualizaciones automáticamente cuando se inicia OpenCode",
|
|
||||||
"settings.updates.row.check.title": "Buscar actualizaciones",
|
|
||||||
"settings.updates.row.check.description": "Buscar actualizaciones manualmente e instalarlas si hay alguna",
|
|
||||||
"settings.updates.action.checkNow": "Buscar ahora",
|
|
||||||
"settings.updates.action.checking": "Buscando...",
|
|
||||||
"settings.updates.toast.latest.title": "Estás al día",
|
|
||||||
"settings.updates.toast.latest.description": "Estás usando la última versión de OpenCode.",
|
|
||||||
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -333,7 +333,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "Langue",
|
"toast.language.title": "Langue",
|
||||||
"toast.language.description": "Passé à {{language}}",
|
"toast.language.description": "Passé à {{language}}",
|
||||||
@@ -527,7 +526,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "Apparence",
|
"settings.general.section.appearance": "Apparence",
|
||||||
"settings.general.section.notifications": "Notifications système",
|
"settings.general.section.notifications": "Notifications système",
|
||||||
"settings.general.section.updates": "Mises à jour",
|
|
||||||
"settings.general.section.sounds": "Effets sonores",
|
"settings.general.section.sounds": "Effets sonores",
|
||||||
|
|
||||||
"settings.general.row.language.title": "Langue",
|
"settings.general.row.language.title": "Langue",
|
||||||
@@ -539,18 +537,6 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Police",
|
"settings.general.row.font.title": "Police",
|
||||||
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
|
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "Notes de version",
|
|
||||||
"settings.general.row.releaseNotes.description": 'Afficher des pop-ups "Quoi de neuf" après les mises à jour',
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "Vérifier les mises à jour au démarrage",
|
|
||||||
"settings.updates.row.startup.description": "Vérifier automatiquement les mises à jour au lancement d'OpenCode",
|
|
||||||
"settings.updates.row.check.title": "Vérifier les mises à jour",
|
|
||||||
"settings.updates.row.check.description": "Vérifier manuellement les mises à jour et installer si disponible",
|
|
||||||
"settings.updates.action.checkNow": "Vérifier maintenant",
|
|
||||||
"settings.updates.action.checking": "Vérification...",
|
|
||||||
"settings.updates.toast.latest.title": "Vous êtes à jour",
|
|
||||||
"settings.updates.toast.latest.description": "Vous utilisez la dernière version d'OpenCode.",
|
|
||||||
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -331,7 +331,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "言語",
|
"toast.language.title": "言語",
|
||||||
"toast.language.description": "{{language}}に切り替えました",
|
"toast.language.description": "{{language}}に切り替えました",
|
||||||
@@ -517,7 +516,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "外観",
|
"settings.general.section.appearance": "外観",
|
||||||
"settings.general.section.notifications": "システム通知",
|
"settings.general.section.notifications": "システム通知",
|
||||||
"settings.general.section.updates": "アップデート",
|
|
||||||
"settings.general.section.sounds": "効果音",
|
"settings.general.section.sounds": "効果音",
|
||||||
|
|
||||||
"settings.general.row.language.title": "言語",
|
"settings.general.row.language.title": "言語",
|
||||||
@@ -529,18 +527,6 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "フォント",
|
"settings.general.row.font.title": "フォント",
|
||||||
"settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします",
|
"settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "リリースノート",
|
|
||||||
"settings.general.row.releaseNotes.description": "アップデート後に「新機能」ポップアップを表示",
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "起動時にアップデートを確認",
|
|
||||||
"settings.updates.row.startup.description": "OpenCode の起動時に自動でアップデートを確認します",
|
|
||||||
"settings.updates.row.check.title": "アップデートを確認",
|
|
||||||
"settings.updates.row.check.description": "手動でアップデートを確認し、利用可能ならインストールします",
|
|
||||||
"settings.updates.action.checkNow": "今すぐ確認",
|
|
||||||
"settings.updates.action.checking": "確認中...",
|
|
||||||
"settings.updates.toast.latest.title": "最新です",
|
|
||||||
"settings.updates.toast.latest.description": "OpenCode は最新バージョンです。",
|
|
||||||
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -334,7 +334,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "언어",
|
"toast.language.title": "언어",
|
||||||
"toast.language.description": "{{language}}(으)로 전환됨",
|
"toast.language.description": "{{language}}(으)로 전환됨",
|
||||||
@@ -518,7 +517,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "모양",
|
"settings.general.section.appearance": "모양",
|
||||||
"settings.general.section.notifications": "시스템 알림",
|
"settings.general.section.notifications": "시스템 알림",
|
||||||
"settings.general.section.updates": "업데이트",
|
|
||||||
"settings.general.section.sounds": "효과음",
|
"settings.general.section.sounds": "효과음",
|
||||||
|
|
||||||
"settings.general.row.language.title": "언어",
|
"settings.general.row.language.title": "언어",
|
||||||
@@ -530,18 +528,6 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "글꼴",
|
"settings.general.row.font.title": "글꼴",
|
||||||
"settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정",
|
"settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "릴리스 노트",
|
|
||||||
"settings.general.row.releaseNotes.description": "업데이트 후 '새 소식' 팝업 표시",
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "시작 시 업데이트 확인",
|
|
||||||
"settings.updates.row.startup.description": "OpenCode를 실행할 때 업데이트를 자동으로 확인합니다",
|
|
||||||
"settings.updates.row.check.title": "업데이트 확인",
|
|
||||||
"settings.updates.row.check.description": "업데이트를 수동으로 확인하고, 사용 가능하면 설치합니다",
|
|
||||||
"settings.updates.action.checkNow": "지금 확인",
|
|
||||||
"settings.updates.action.checking": "확인 중...",
|
|
||||||
"settings.updates.toast.latest.title": "최신 상태입니다",
|
|
||||||
"settings.updates.toast.latest.description": "현재 최신 버전의 OpenCode를 사용 중입니다.",
|
|
||||||
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -334,7 +334,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "Språk",
|
"toast.language.title": "Språk",
|
||||||
"toast.language.description": "Byttet til {{language}}",
|
"toast.language.description": "Byttet til {{language}}",
|
||||||
@@ -520,7 +519,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "Utseende",
|
"settings.general.section.appearance": "Utseende",
|
||||||
"settings.general.section.notifications": "Systemvarsler",
|
"settings.general.section.notifications": "Systemvarsler",
|
||||||
"settings.general.section.updates": "Oppdateringer",
|
|
||||||
"settings.general.section.sounds": "Lydeffekter",
|
"settings.general.section.sounds": "Lydeffekter",
|
||||||
|
|
||||||
"settings.general.row.language.title": "Språk",
|
"settings.general.row.language.title": "Språk",
|
||||||
@@ -532,18 +530,6 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Skrift",
|
"settings.general.row.font.title": "Skrift",
|
||||||
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
|
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "Utgivelsesnotater",
|
|
||||||
"settings.general.row.releaseNotes.description": 'Vis "Hva er nytt"-vinduer etter oppdateringer',
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "Se etter oppdateringer ved oppstart",
|
|
||||||
"settings.updates.row.startup.description": "Se automatisk etter oppdateringer når OpenCode starter",
|
|
||||||
"settings.updates.row.check.title": "Se etter oppdateringer",
|
|
||||||
"settings.updates.row.check.description": "Se etter oppdateringer manuelt og installer hvis tilgjengelig",
|
|
||||||
"settings.updates.action.checkNow": "Sjekk nå",
|
|
||||||
"settings.updates.action.checking": "Sjekker...",
|
|
||||||
"settings.updates.toast.latest.title": "Du er oppdatert",
|
|
||||||
"settings.updates.toast.latest.description": "Du bruker den nyeste versjonen av OpenCode.",
|
|
||||||
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "Język",
|
"toast.language.title": "Język",
|
||||||
"toast.language.description": "Przełączono na {{language}}",
|
"toast.language.description": "Przełączono na {{language}}",
|
||||||
@@ -519,7 +518,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "Wygląd",
|
"settings.general.section.appearance": "Wygląd",
|
||||||
"settings.general.section.notifications": "Powiadomienia systemowe",
|
"settings.general.section.notifications": "Powiadomienia systemowe",
|
||||||
"settings.general.section.updates": "Aktualizacje",
|
|
||||||
"settings.general.section.sounds": "Efekty dźwiękowe",
|
"settings.general.section.sounds": "Efekty dźwiękowe",
|
||||||
|
|
||||||
"settings.general.row.language.title": "Język",
|
"settings.general.row.language.title": "Język",
|
||||||
@@ -530,18 +528,6 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
|
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
|
||||||
"settings.general.row.font.title": "Czcionka",
|
"settings.general.row.font.title": "Czcionka",
|
||||||
"settings.general.row.font.description": "Dostosuj czcionkę mono używaną w blokach kodu",
|
"settings.general.row.font.description": "Dostosuj czcionkę mono używaną w blokach kodu",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "Informacje o wydaniu",
|
|
||||||
"settings.general.row.releaseNotes.description": 'Pokazuj wyskakujące okna "Co nowego" po aktualizacjach',
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "Sprawdzaj aktualizacje przy uruchomieniu",
|
|
||||||
"settings.updates.row.startup.description": "Automatycznie sprawdzaj aktualizacje podczas uruchamiania OpenCode",
|
|
||||||
"settings.updates.row.check.title": "Sprawdź aktualizacje",
|
|
||||||
"settings.updates.row.check.description": "Ręcznie sprawdź aktualizacje i zainstaluj, jeśli są dostępne",
|
|
||||||
"settings.updates.action.checkNow": "Sprawdź teraz",
|
|
||||||
"settings.updates.action.checking": "Sprawdzanie...",
|
|
||||||
"settings.updates.toast.latest.title": "Masz najnowszą wersję",
|
|
||||||
"settings.updates.toast.latest.description": "Korzystasz z najnowszej wersji OpenCode.",
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -333,7 +333,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "Язык",
|
"toast.language.title": "Язык",
|
||||||
"toast.language.description": "Переключено на {{language}}",
|
"toast.language.description": "Переключено на {{language}}",
|
||||||
@@ -522,7 +521,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "Внешний вид",
|
"settings.general.section.appearance": "Внешний вид",
|
||||||
"settings.general.section.notifications": "Системные уведомления",
|
"settings.general.section.notifications": "Системные уведомления",
|
||||||
"settings.general.section.updates": "Обновления",
|
|
||||||
"settings.general.section.sounds": "Звуковые эффекты",
|
"settings.general.section.sounds": "Звуковые эффекты",
|
||||||
|
|
||||||
"settings.general.row.language.title": "Язык",
|
"settings.general.row.language.title": "Язык",
|
||||||
@@ -533,18 +531,6 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "Настройте оформление OpenCode.",
|
"settings.general.row.theme.description": "Настройте оформление OpenCode.",
|
||||||
"settings.general.row.font.title": "Шрифт",
|
"settings.general.row.font.title": "Шрифт",
|
||||||
"settings.general.row.font.description": "Настройте моноширинный шрифт для блоков кода",
|
"settings.general.row.font.description": "Настройте моноширинный шрифт для блоков кода",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "Примечания к выпуску",
|
|
||||||
"settings.general.row.releaseNotes.description": 'Показывать всплывающие окна "Что нового" после обновлений',
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "Проверять обновления при запуске",
|
|
||||||
"settings.updates.row.startup.description": "Автоматически проверять обновления при запуске OpenCode",
|
|
||||||
"settings.updates.row.check.title": "Проверить обновления",
|
|
||||||
"settings.updates.row.check.description": "Проверить обновления вручную и установить, если доступны",
|
|
||||||
"settings.updates.action.checkNow": "Проверить сейчас",
|
|
||||||
"settings.updates.action.checking": "Проверка...",
|
|
||||||
"settings.updates.toast.latest.title": "У вас последняя версия",
|
|
||||||
"settings.updates.toast.latest.description": "Вы используете последнюю версию OpenCode.",
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -1,718 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"command.category.suggested": "แนะนำ",
|
|
||||||
"command.category.view": "มุมมอง",
|
|
||||||
"command.category.project": "โปรเจกต์",
|
|
||||||
"command.category.provider": "ผู้ให้บริการ",
|
|
||||||
"command.category.server": "เซิร์ฟเวอร์",
|
|
||||||
"command.category.session": "เซสชัน",
|
|
||||||
"command.category.theme": "ธีม",
|
|
||||||
"command.category.language": "ภาษา",
|
|
||||||
"command.category.file": "ไฟล์",
|
|
||||||
"command.category.context": "บริบท",
|
|
||||||
"command.category.terminal": "เทอร์มินัล",
|
|
||||||
"command.category.model": "โมเดล",
|
|
||||||
"command.category.mcp": "MCP",
|
|
||||||
"command.category.agent": "เอเจนต์",
|
|
||||||
"command.category.permissions": "สิทธิ์",
|
|
||||||
"command.category.workspace": "พื้นที่ทำงาน",
|
|
||||||
"command.category.settings": "การตั้งค่า",
|
|
||||||
|
|
||||||
"theme.scheme.system": "ระบบ",
|
|
||||||
"theme.scheme.light": "สว่าง",
|
|
||||||
"theme.scheme.dark": "มืด",
|
|
||||||
|
|
||||||
"command.sidebar.toggle": "สลับแถบข้าง",
|
|
||||||
"command.project.open": "เปิดโปรเจกต์",
|
|
||||||
"command.provider.connect": "เชื่อมต่อผู้ให้บริการ",
|
|
||||||
"command.server.switch": "สลับเซิร์ฟเวอร์",
|
|
||||||
"command.settings.open": "เปิดการตั้งค่า",
|
|
||||||
"command.session.previous": "เซสชันก่อนหน้า",
|
|
||||||
"command.session.next": "เซสชันถัดไป",
|
|
||||||
"command.session.archive": "จัดเก็บเซสชัน",
|
|
||||||
|
|
||||||
"command.palette": "คำสั่งค้นหา",
|
|
||||||
|
|
||||||
"command.theme.cycle": "เปลี่ยนธีม",
|
|
||||||
"command.theme.set": "ใช้ธีม: {{theme}}",
|
|
||||||
"command.theme.scheme.cycle": "เปลี่ยนโทนสี",
|
|
||||||
"command.theme.scheme.set": "ใช้โทนสี: {{scheme}}",
|
|
||||||
|
|
||||||
"command.language.cycle": "เปลี่ยนภาษา",
|
|
||||||
"command.language.set": "ใช้ภาษา: {{language}}",
|
|
||||||
|
|
||||||
"command.session.new": "เซสชันใหม่",
|
|
||||||
"command.file.open": "เปิดไฟล์",
|
|
||||||
"command.file.open.description": "ค้นหาไฟล์และคำสั่ง",
|
|
||||||
"command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท",
|
|
||||||
"command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน",
|
|
||||||
"command.terminal.toggle": "สลับเทอร์มินัล",
|
|
||||||
"command.fileTree.toggle": "สลับต้นไม้ไฟล์",
|
|
||||||
"command.review.toggle": "สลับการตรวจสอบ",
|
|
||||||
"command.terminal.new": "เทอร์มินัลใหม่",
|
|
||||||
"command.terminal.new.description": "สร้างแท็บเทอร์มินัลใหม่",
|
|
||||||
"command.steps.toggle": "สลับขั้นตอน",
|
|
||||||
"command.steps.toggle.description": "แสดงหรือซ่อนขั้นตอนสำหรับข้อความปัจจุบัน",
|
|
||||||
"command.message.previous": "ข้อความก่อนหน้า",
|
|
||||||
"command.message.previous.description": "ไปที่ข้อความผู้ใช้ก่อนหน้า",
|
|
||||||
"command.message.next": "ข้อความถัดไป",
|
|
||||||
"command.message.next.description": "ไปที่ข้อความผู้ใช้ถัดไป",
|
|
||||||
"command.model.choose": "เลือกโมเดล",
|
|
||||||
"command.model.choose.description": "เลือกโมเดลอื่น",
|
|
||||||
"command.mcp.toggle": "สลับ MCPs",
|
|
||||||
"command.mcp.toggle.description": "สลับ MCPs",
|
|
||||||
"command.agent.cycle": "เปลี่ยนเอเจนต์",
|
|
||||||
"command.agent.cycle.description": "สลับไปยังเอเจนต์ถัดไป",
|
|
||||||
"command.agent.cycle.reverse": "เปลี่ยนเอเจนต์ย้อนกลับ",
|
|
||||||
"command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า",
|
|
||||||
"command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด",
|
|
||||||
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
|
||||||
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
|
||||||
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
|
||||||
"command.session.undo": "ยกเลิก",
|
|
||||||
"command.session.undo.description": "ยกเลิกข้อความล่าสุด",
|
|
||||||
"command.session.redo": "ทำซ้ำ",
|
|
||||||
"command.session.redo.description": "ทำซ้ำข้อความที่ถูกยกเลิกล่าสุด",
|
|
||||||
"command.session.compact": "บีบอัดเซสชัน",
|
|
||||||
"command.session.compact.description": "สรุปเซสชันเพื่อลดขนาดบริบท",
|
|
||||||
"command.session.fork": "แตกแขนงจากข้อความ",
|
|
||||||
"command.session.fork.description": "สร้างเซสชันใหม่จากข้อความก่อนหน้า",
|
|
||||||
"command.session.share": "แชร์เซสชัน",
|
|
||||||
"command.session.share.description": "แชร์เซสชันนี้และคัดลอก URL ไปยังคลิปบอร์ด",
|
|
||||||
"command.session.unshare": "ยกเลิกการแชร์เซสชัน",
|
|
||||||
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
|
||||||
|
|
||||||
"palette.search.placeholder": "ค้นหาไฟล์และคำสั่ง",
|
|
||||||
"palette.empty": "ไม่พบผลลัพธ์",
|
|
||||||
"palette.group.commands": "คำสั่ง",
|
|
||||||
"palette.group.files": "ไฟล์",
|
|
||||||
|
|
||||||
"dialog.provider.search.placeholder": "ค้นหาผู้ให้บริการ",
|
|
||||||
"dialog.provider.empty": "ไม่พบผู้ให้บริการ",
|
|
||||||
"dialog.provider.group.popular": "ยอดนิยม",
|
|
||||||
"dialog.provider.group.other": "อื่น ๆ",
|
|
||||||
"dialog.provider.tag.recommended": "แนะนำ",
|
|
||||||
"dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ",
|
|
||||||
"dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max",
|
|
||||||
"dialog.provider.copilot.note": "โมเดล Claude สำหรับการช่วยเหลือในการเขียนโค้ด",
|
|
||||||
"dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ",
|
|
||||||
"dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง",
|
|
||||||
"dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว",
|
|
||||||
"dialog.provider.vercel.note": "การเข้าถึงโมเดล AI แบบรวมด้วยการกำหนดเส้นทางอัจฉริยะ",
|
|
||||||
|
|
||||||
"dialog.model.select.title": "เลือกโมเดล",
|
|
||||||
"dialog.model.search.placeholder": "ค้นหาโมเดล",
|
|
||||||
"dialog.model.empty": "ไม่พบผลลัพธ์โมเดล",
|
|
||||||
"dialog.model.manage": "จัดการโมเดล",
|
|
||||||
"dialog.model.manage.description": "ปรับแต่งโมเดลที่จะปรากฏในตัวเลือกโมเดล",
|
|
||||||
|
|
||||||
"dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย OpenCode",
|
|
||||||
"dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม",
|
|
||||||
|
|
||||||
"dialog.provider.viewAll": "แสดงผู้ให้บริการเพิ่มเติม",
|
|
||||||
|
|
||||||
"provider.connect.title": "เชื่อมต่อ {{provider}}",
|
|
||||||
"provider.connect.title.anthropicProMax": "เข้าสู่ระบบด้วย Claude Pro/Max",
|
|
||||||
"provider.connect.selectMethod": "เลือกวิธีการเข้าสู่ระบบสำหรับ {{provider}}",
|
|
||||||
"provider.connect.method.apiKey": "คีย์ API",
|
|
||||||
"provider.connect.status.inProgress": "กำลังอนุญาต...",
|
|
||||||
"provider.connect.status.waiting": "รอการอนุญาต...",
|
|
||||||
"provider.connect.status.failed": "การอนุญาตล้มเหลว: {{error}}",
|
|
||||||
"provider.connect.apiKey.description":
|
|
||||||
"ป้อนคีย์ API ของ {{provider}} เพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน OpenCode",
|
|
||||||
"provider.connect.apiKey.label": "คีย์ API ของ {{provider}}",
|
|
||||||
"provider.connect.apiKey.placeholder": "คีย์ API",
|
|
||||||
"provider.connect.apiKey.required": "ต้องใช้คีย์ API",
|
|
||||||
"provider.connect.opencodeZen.line1":
|
|
||||||
"OpenCode Zen ให้คุณเข้าถึงชุดโมเดลที่เชื่อถือได้และปรับแต่งแล้วสำหรับเอเจนต์การเขียนโค้ด",
|
|
||||||
"provider.connect.opencodeZen.line2":
|
|
||||||
"ด้วยคีย์ API เดียวคุณจะได้รับการเข้าถึงโมเดล เช่น Claude, GPT, Gemini, GLM และอื่น ๆ",
|
|
||||||
"provider.connect.opencodeZen.visit.prefix": "เยี่ยมชม ",
|
|
||||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
|
||||||
"provider.connect.opencodeZen.visit.suffix": " เพื่อรวบรวมคีย์ API ของคุณ",
|
|
||||||
"provider.connect.oauth.code.visit.prefix": "เยี่ยมชม ",
|
|
||||||
"provider.connect.oauth.code.visit.link": "ลิงก์นี้",
|
|
||||||
"provider.connect.oauth.code.visit.suffix":
|
|
||||||
" เพื่อรวบรวมรหัสการอนุญาตของคุณเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน OpenCode",
|
|
||||||
"provider.connect.oauth.code.label": "รหัสการอนุญาต {{method}}",
|
|
||||||
"provider.connect.oauth.code.placeholder": "รหัสการอนุญาต",
|
|
||||||
"provider.connect.oauth.code.required": "ต้องใช้รหัสการอนุญาต",
|
|
||||||
"provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง",
|
|
||||||
"provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ",
|
|
||||||
"provider.connect.oauth.auto.visit.link": "ลิงก์นี้",
|
|
||||||
"provider.connect.oauth.auto.visit.suffix":
|
|
||||||
" และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน OpenCode",
|
|
||||||
"provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน",
|
|
||||||
"provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว",
|
|
||||||
"provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว",
|
|
||||||
|
|
||||||
"provider.disconnect.toast.disconnected.title": "{{provider}} ที่ยกเลิกการเชื่อมต่อแล้ว",
|
|
||||||
"provider.disconnect.toast.disconnected.description": "โมเดล {{provider}} ไม่พร้อมใช้งานอีกต่อไป",
|
|
||||||
|
|
||||||
"model.tag.free": "ฟรี",
|
|
||||||
"model.tag.latest": "ล่าสุด",
|
|
||||||
"model.provider.anthropic": "Anthropic",
|
|
||||||
"model.provider.openai": "OpenAI",
|
|
||||||
"model.provider.google": "Google",
|
|
||||||
"model.provider.xai": "xAI",
|
|
||||||
"model.provider.meta": "Meta",
|
|
||||||
"model.input.text": "ข้อความ",
|
|
||||||
"model.input.image": "รูปภาพ",
|
|
||||||
"model.input.audio": "เสียง",
|
|
||||||
"model.input.video": "วิดีโอ",
|
|
||||||
"model.input.pdf": "pdf",
|
|
||||||
"model.tooltip.allows": "อนุญาต: {{inputs}}",
|
|
||||||
"model.tooltip.reasoning.allowed": "อนุญาตการใช้เหตุผล",
|
|
||||||
"model.tooltip.reasoning.none": "ไม่มีการใช้เหตุผล",
|
|
||||||
"model.tooltip.context": "ขีดจำกัดบริบท {{limit}}",
|
|
||||||
|
|
||||||
"common.search.placeholder": "ค้นหา",
|
|
||||||
"common.goBack": "ย้อนกลับ",
|
|
||||||
"common.loading": "กำลังโหลด",
|
|
||||||
"common.loading.ellipsis": "...",
|
|
||||||
"common.cancel": "ยกเลิก",
|
|
||||||
"common.connect": "เชื่อมต่อ",
|
|
||||||
"common.disconnect": "ยกเลิกการเชื่อมต่อ",
|
|
||||||
"common.submit": "ส่ง",
|
|
||||||
"common.save": "บันทึก",
|
|
||||||
"common.saving": "กำลังบันทึก...",
|
|
||||||
"common.default": "ค่าเริ่มต้น",
|
|
||||||
"common.attachment": "ไฟล์แนบ",
|
|
||||||
|
|
||||||
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์...",
|
|
||||||
"prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"',
|
|
||||||
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
|
||||||
"prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…",
|
|
||||||
"prompt.mode.shell": "เชลล์",
|
|
||||||
"prompt.mode.shell.exit": "กด esc เพื่อออก",
|
|
||||||
|
|
||||||
"prompt.example.1": "แก้ไข TODO ในโค้ดเบส",
|
|
||||||
"prompt.example.2": "เทคโนโลยีของโปรเจกต์นี้คืออะไร?",
|
|
||||||
"prompt.example.3": "แก้ไขการทดสอบที่เสีย",
|
|
||||||
"prompt.example.4": "อธิบายวิธีการทำงานของการตรวจสอบสิทธิ์",
|
|
||||||
"prompt.example.5": "ค้นหาและแก้ไขช่องโหว่ความปลอดภัย",
|
|
||||||
"prompt.example.6": "เพิ่มการทดสอบหน่วยสำหรับบริการผู้ใช้",
|
|
||||||
"prompt.example.7": "ปรับโครงสร้างฟังก์ชันนี้ให้อ่านง่ายขึ้น",
|
|
||||||
"prompt.example.8": "ข้อผิดพลาดนี้หมายความว่าอะไร?",
|
|
||||||
"prompt.example.9": "ช่วยฉันดีบักปัญหานี้",
|
|
||||||
"prompt.example.10": "สร้างเอกสาร API",
|
|
||||||
"prompt.example.11": "ปรับปรุงการสืบค้นฐานข้อมูล",
|
|
||||||
"prompt.example.12": "เพิ่มการตรวจสอบข้อมูลนำเข้า",
|
|
||||||
"prompt.example.13": "สร้างคอมโพเนนต์ใหม่สำหรับ...",
|
|
||||||
"prompt.example.14": "ฉันจะทำให้โปรเจกต์นี้ทำงานได้อย่างไร?",
|
|
||||||
"prompt.example.15": "ตรวจสอบโค้ดของฉันเพื่อแนวทางปฏิบัติที่ดีที่สุด",
|
|
||||||
"prompt.example.16": "เพิ่มการจัดการข้อผิดพลาดในฟังก์ชันนี้",
|
|
||||||
"prompt.example.17": "อธิบายรูปแบบ regex นี้",
|
|
||||||
"prompt.example.18": "แปลงสิ่งนี้เป็น TypeScript",
|
|
||||||
"prompt.example.19": "เพิ่มการบันทึกทั่วทั้งโค้ดเบส",
|
|
||||||
"prompt.example.20": "มีการพึ่งพาอะไรที่ล้าสมัยอยู่?",
|
|
||||||
"prompt.example.21": "ช่วยฉันเขียนสคริปต์การย้ายข้อมูล",
|
|
||||||
"prompt.example.22": "ใช้งานแคชสำหรับจุดสิ้นสุดนี้",
|
|
||||||
"prompt.example.23": "เพิ่มการแบ่งหน้าในรายการนี้",
|
|
||||||
"prompt.example.24": "สร้างคำสั่ง CLI สำหรับ...",
|
|
||||||
"prompt.example.25": "ตัวแปรสภาพแวดล้อมทำงานอย่างไรที่นี่?",
|
|
||||||
|
|
||||||
"prompt.popover.emptyResults": "ไม่พบผลลัพธ์ที่ตรงกัน",
|
|
||||||
"prompt.popover.emptyCommands": "ไม่พบคำสั่งที่ตรงกัน",
|
|
||||||
"prompt.dropzone.label": "วางรูปภาพหรือ PDF ที่นี่",
|
|
||||||
"prompt.slash.badge.custom": "กำหนดเอง",
|
|
||||||
"prompt.context.active": "ใช้งานอยู่",
|
|
||||||
"prompt.context.includeActiveFile": "รวมไฟล์ที่ใช้งานอยู่",
|
|
||||||
"prompt.context.removeActiveFile": "เอาไฟล์ที่ใช้งานอยู่ออกจากบริบท",
|
|
||||||
"prompt.context.removeFile": "เอาไฟล์ออกจากบริบท",
|
|
||||||
"prompt.action.attachFile": "แนบไฟล์",
|
|
||||||
"prompt.attachment.remove": "เอาไฟล์แนบออก",
|
|
||||||
"prompt.action.send": "ส่ง",
|
|
||||||
"prompt.action.stop": "หยุด",
|
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "การวางไม่รองรับ",
|
|
||||||
"prompt.toast.pasteUnsupported.description": "สามารถวางรูปภาพหรือ PDF เท่านั้น",
|
|
||||||
"prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล",
|
|
||||||
"prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์",
|
|
||||||
"prompt.toast.worktreeCreateFailed.title": "ไม่สามารถสร้าง worktree",
|
|
||||||
"prompt.toast.sessionCreateFailed.title": "ไม่สามารถสร้างเซสชัน",
|
|
||||||
"prompt.toast.shellSendFailed.title": "ไม่สามารถส่งคำสั่งเชลล์",
|
|
||||||
"prompt.toast.commandSendFailed.title": "ไม่สามารถส่งคำสั่ง",
|
|
||||||
"prompt.toast.promptSendFailed.title": "ไม่สามารถส่งพร้อมท์",
|
|
||||||
|
|
||||||
"dialog.mcp.title": "MCPs",
|
|
||||||
"dialog.mcp.description": "{{enabled}} จาก {{total}} ที่เปิดใช้งาน",
|
|
||||||
"dialog.mcp.empty": "ไม่มี MCP ที่กำหนดค่า",
|
|
||||||
|
|
||||||
"dialog.lsp.empty": "LSPs ตรวจจับอัตโนมัติจากประเภทไฟล์",
|
|
||||||
"dialog.plugins.empty": "ปลั๊กอินที่กำหนดค่าใน opencode.json",
|
|
||||||
|
|
||||||
"mcp.status.connected": "เชื่อมต่อแล้ว",
|
|
||||||
"mcp.status.failed": "ล้มเหลว",
|
|
||||||
"mcp.status.needs_auth": "ต้องการการตรวจสอบสิทธิ์",
|
|
||||||
"mcp.status.disabled": "ปิดใช้งาน",
|
|
||||||
|
|
||||||
"dialog.fork.empty": "ไม่มีข้อความให้แตกแขนง",
|
|
||||||
|
|
||||||
"dialog.directory.search.placeholder": "ค้นหาโฟลเดอร์",
|
|
||||||
"dialog.directory.empty": "ไม่พบโฟลเดอร์",
|
|
||||||
|
|
||||||
"dialog.server.title": "เซิร์ฟเวอร์",
|
|
||||||
"dialog.server.description": "สลับเซิร์ฟเวอร์ OpenCode ที่แอปนี้เชื่อมต่อด้วย",
|
|
||||||
"dialog.server.search.placeholder": "ค้นหาเซิร์ฟเวอร์",
|
|
||||||
"dialog.server.empty": "ยังไม่มีเซิร์ฟเวอร์",
|
|
||||||
"dialog.server.add.title": "เพิ่มเซิร์ฟเวอร์",
|
|
||||||
"dialog.server.add.url": "URL เซิร์ฟเวอร์",
|
|
||||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
|
||||||
"dialog.server.add.error": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์",
|
|
||||||
"dialog.server.add.checking": "กำลังตรวจสอบ...",
|
|
||||||
"dialog.server.add.button": "เพิ่มเซิร์ฟเวอร์",
|
|
||||||
"dialog.server.default.title": "เซิร์ฟเวอร์เริ่มต้น",
|
|
||||||
"dialog.server.default.description":
|
|
||||||
"เชื่อมต่อกับเซิร์ฟเวอร์นี้เมื่อเปิดแอปแทนการเริ่มเซิร์ฟเวอร์ในเครื่อง ต้องรีสตาร์ท",
|
|
||||||
"dialog.server.default.none": "ไม่ได้เลือกเซิร์ฟเวอร์",
|
|
||||||
"dialog.server.default.set": "ตั้งเซิร์ฟเวอร์ปัจจุบันเป็นค่าเริ่มต้น",
|
|
||||||
"dialog.server.default.clear": "ล้าง",
|
|
||||||
"dialog.server.action.remove": "เอาเซิร์ฟเวอร์ออก",
|
|
||||||
|
|
||||||
"dialog.server.menu.edit": "แก้ไข",
|
|
||||||
"dialog.server.menu.default": "ตั้งเป็นค่าเริ่มต้น",
|
|
||||||
"dialog.server.menu.defaultRemove": "เอาค่าเริ่มต้นออก",
|
|
||||||
"dialog.server.menu.delete": "ลบ",
|
|
||||||
"dialog.server.current": "เซิร์ฟเวอร์ปัจจุบัน",
|
|
||||||
"dialog.server.status.default": "ค่าเริ่มต้น",
|
|
||||||
|
|
||||||
"dialog.project.edit.title": "แก้ไขโปรเจกต์",
|
|
||||||
"dialog.project.edit.name": "ชื่อ",
|
|
||||||
"dialog.project.edit.icon": "ไอคอน",
|
|
||||||
"dialog.project.edit.icon.alt": "ไอคอนโปรเจกต์",
|
|
||||||
"dialog.project.edit.icon.hint": "คลิกหรือลากรูปภาพ",
|
|
||||||
"dialog.project.edit.icon.recommended": "แนะนำ: 128x128px",
|
|
||||||
"dialog.project.edit.color": "สี",
|
|
||||||
"dialog.project.edit.color.select": "เลือกสี {{color}}",
|
|
||||||
"dialog.project.edit.worktree.startup": "สคริปต์เริ่มต้นพื้นที่ทำงาน",
|
|
||||||
"dialog.project.edit.worktree.startup.description": "ทำงานหลังจากสร้างพื้นที่ทำงานใหม่ (worktree)",
|
|
||||||
"dialog.project.edit.worktree.startup.placeholder": "เช่น bun install",
|
|
||||||
|
|
||||||
"context.breakdown.title": "การแบ่งบริบท",
|
|
||||||
"context.breakdown.note": 'การแบ่งโดยประมาณของโทเค็นนำเข้า "อื่น ๆ" รวมถึงคำนิยามเครื่องมือและโอเวอร์เฮด',
|
|
||||||
"context.breakdown.system": "ระบบ",
|
|
||||||
"context.breakdown.user": "ผู้ใช้",
|
|
||||||
"context.breakdown.assistant": "ผู้ช่วย",
|
|
||||||
"context.breakdown.tool": "การเรียกเครื่องมือ",
|
|
||||||
"context.breakdown.other": "อื่น ๆ",
|
|
||||||
|
|
||||||
"context.systemPrompt.title": "พร้อมท์ระบบ",
|
|
||||||
"context.rawMessages.title": "ข้อความดิบ",
|
|
||||||
|
|
||||||
"context.stats.session": "เซสชัน",
|
|
||||||
"context.stats.messages": "ข้อความ",
|
|
||||||
"context.stats.provider": "ผู้ให้บริการ",
|
|
||||||
"context.stats.model": "โมเดล",
|
|
||||||
"context.stats.limit": "ขีดจำกัดบริบท",
|
|
||||||
"context.stats.totalTokens": "โทเค็นทั้งหมด",
|
|
||||||
"context.stats.usage": "การใช้งาน",
|
|
||||||
"context.stats.inputTokens": "โทเค็นนำเข้า",
|
|
||||||
"context.stats.outputTokens": "โทเค็นส่งออก",
|
|
||||||
"context.stats.reasoningTokens": "โทเค็นการใช้เหตุผล",
|
|
||||||
"context.stats.cacheTokens": "โทเค็นแคช (อ่าน/เขียน)",
|
|
||||||
"context.stats.userMessages": "ข้อความผู้ใช้",
|
|
||||||
"context.stats.assistantMessages": "ข้อความผู้ช่วย",
|
|
||||||
"context.stats.totalCost": "ต้นทุนทั้งหมด",
|
|
||||||
"context.stats.sessionCreated": "สร้างเซสชันเมื่อ",
|
|
||||||
"context.stats.lastActivity": "กิจกรรมล่าสุด",
|
|
||||||
|
|
||||||
"context.usage.tokens": "โทเค็น",
|
|
||||||
"context.usage.usage": "การใช้งาน",
|
|
||||||
"context.usage.cost": "ต้นทุน",
|
|
||||||
"context.usage.clickToView": "คลิกเพื่อดูบริบท",
|
|
||||||
"context.usage.view": "ดูการใช้บริบท",
|
|
||||||
|
|
||||||
"language.en": "อังกฤษ",
|
|
||||||
"language.zh": "จีนตัวย่อ",
|
|
||||||
"language.zht": "จีนตัวเต็ม",
|
|
||||||
"language.ko": "เกาหลี",
|
|
||||||
"language.de": "เยอรมัน",
|
|
||||||
"language.es": "สเปน",
|
|
||||||
"language.fr": "ฝรั่งเศส",
|
|
||||||
"language.da": "เดนมาร์ก",
|
|
||||||
"language.ja": "ญี่ปุ่น",
|
|
||||||
"language.pl": "โปแลนด์",
|
|
||||||
"language.ru": "รัสเซีย",
|
|
||||||
"language.ar": "อาหรับ",
|
|
||||||
"language.no": "นอร์เวย์",
|
|
||||||
"language.br": "โปรตุเกส (บราซิล)",
|
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "ภาษา",
|
|
||||||
"toast.language.description": "สลับไปที่ {{language}}",
|
|
||||||
|
|
||||||
"toast.theme.title": "สลับธีมแล้ว",
|
|
||||||
"toast.scheme.title": "โทนสี",
|
|
||||||
|
|
||||||
"toast.permissions.autoaccept.on.title": "กำลังยอมรับการแก้ไขโดยอัตโนมัติ",
|
|
||||||
"toast.permissions.autoaccept.on.description": "สิทธิ์การแก้ไขและเขียนจะได้รับการอนุมัติโดยอัตโนมัติ",
|
|
||||||
"toast.permissions.autoaccept.off.title": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
|
||||||
"toast.permissions.autoaccept.off.description": "สิทธิ์การแก้ไขและเขียนจะต้องได้รับการอนุมัติ",
|
|
||||||
|
|
||||||
"toast.model.none.title": "ไม่ได้เลือกโมเดล",
|
|
||||||
"toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้",
|
|
||||||
|
|
||||||
"toast.file.loadFailed.title": "ไม่สามารถโหลดไฟล์",
|
|
||||||
"toast.file.listFailed.title": "ไม่สามารถแสดงรายการไฟล์",
|
|
||||||
|
|
||||||
"toast.context.noLineSelection.title": "ไม่มีการเลือกบรรทัด",
|
|
||||||
"toast.context.noLineSelection.description": "เลือกช่วงบรรทัดในแท็บไฟล์ก่อน",
|
|
||||||
|
|
||||||
"toast.session.share.copyFailed.title": "ไม่สามารถคัดลอก URL ไปยังคลิปบอร์ด",
|
|
||||||
"toast.session.share.success.title": "แชร์เซสชันแล้ว",
|
|
||||||
"toast.session.share.success.description": "คัดลอก URL แชร์ไปยังคลิปบอร์ดแล้ว!",
|
|
||||||
"toast.session.share.failed.title": "ไม่สามารถแชร์เซสชัน",
|
|
||||||
"toast.session.share.failed.description": "เกิดข้อผิดพลาดระหว่างการแชร์เซสชัน",
|
|
||||||
|
|
||||||
"toast.session.unshare.success.title": "ยกเลิกการแชร์เซสชันแล้ว",
|
|
||||||
"toast.session.unshare.success.description": "ยกเลิกการแชร์เซสชันสำเร็จ!",
|
|
||||||
"toast.session.unshare.failed.title": "ไม่สามารถยกเลิกการแชร์เซสชัน",
|
|
||||||
"toast.session.unshare.failed.description": "เกิดข้อผิดพลาดระหว่างการยกเลิกการแชร์เซสชัน",
|
|
||||||
|
|
||||||
"toast.session.listFailed.title": "ไม่สามารถโหลดเซสชันสำหรับ {{project}}",
|
|
||||||
|
|
||||||
"toast.update.title": "มีการอัปเดต",
|
|
||||||
"toast.update.description": "เวอร์ชันใหม่ของ OpenCode ({{version}}) พร้อมใช้งานสำหรับติดตั้ง",
|
|
||||||
"toast.update.action.installRestart": "ติดตั้งและรีสตาร์ท",
|
|
||||||
"toast.update.action.notYet": "ยังไม่",
|
|
||||||
|
|
||||||
"error.page.title": "เกิดข้อผิดพลาด",
|
|
||||||
"error.page.description": "เกิดข้อผิดพลาดระหว่างการโหลดแอปพลิเคชัน",
|
|
||||||
"error.page.details.label": "รายละเอียดข้อผิดพลาด",
|
|
||||||
"error.page.action.restart": "รีสตาร์ท",
|
|
||||||
"error.page.action.checking": "กำลังตรวจสอบ...",
|
|
||||||
"error.page.action.checkUpdates": "ตรวจสอบการอัปเดต",
|
|
||||||
"error.page.action.updateTo": "อัปเดตเป็น {{version}}",
|
|
||||||
"error.page.report.prefix": "โปรดรายงานข้อผิดพลาดนี้ให้ทีม OpenCode",
|
|
||||||
"error.page.report.discord": "บน Discord",
|
|
||||||
"error.page.version": "เวอร์ชัน: {{version}}",
|
|
||||||
|
|
||||||
"error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?",
|
|
||||||
|
|
||||||
"error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?",
|
|
||||||
|
|
||||||
"error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก",
|
|
||||||
"error.chain.causedBy": "สาเหตุ:",
|
|
||||||
"error.chain.apiError": "ข้อผิดพลาด API",
|
|
||||||
"error.chain.status": "สถานะ: {{status}}",
|
|
||||||
"error.chain.retryable": "สามารถลองใหม่: {{retryable}}",
|
|
||||||
"error.chain.responseBody": "เนื้อหาการตอบสนอง:\n{{body}}",
|
|
||||||
"error.chain.didYouMean": "คุณหมายถึง: {{suggestions}}",
|
|
||||||
"error.chain.modelNotFound": "ไม่พบโมเดล: {{provider}}/{{model}}",
|
|
||||||
"error.chain.checkConfig": "ตรวจสอบการกำหนดค่าของคุณ (opencode.json) ชื่อผู้ให้บริการ/โมเดล",
|
|
||||||
"error.chain.mcpFailed": 'เซิร์ฟเวอร์ MCP "{{name}}" ล้มเหลว โปรดทราบว่า OpenCode ยังไม่รองรับการตรวจสอบสิทธิ์ MCP',
|
|
||||||
"error.chain.providerAuthFailed": "การตรวจสอบสิทธิ์ผู้ให้บริการล้มเหลว ({{provider}}): {{message}}",
|
|
||||||
"error.chain.providerInitFailed": 'ไม่สามารถเริ่มต้นผู้ให้บริการ "{{provider}}" ตรวจสอบข้อมูลรับรองและการกำหนดค่า',
|
|
||||||
"error.chain.configJsonInvalid": "ไฟล์กำหนดค่าที่ {{path}} ไม่ใช่ JSON(C) ที่ถูกต้อง",
|
|
||||||
"error.chain.configJsonInvalidWithMessage": "ไฟล์กำหนดค่าที่ {{path}} ไม่ใช่ JSON(C) ที่ถูกต้อง: {{message}}",
|
|
||||||
"error.chain.configDirectoryTypo":
|
|
||||||
'ไดเรกทอรี "{{dir}}" ใน {{path}} ไม่ถูกต้อง เปลี่ยนชื่อไดเรกทอรีเป็น "{{suggestion}}" หรือเอาออก นี่เป็นการสะกดผิดทั่วไป',
|
|
||||||
"error.chain.configFrontmatterError": "ไม่สามารถแยกวิเคราะห์ frontmatter ใน {{path}}:\n{{message}}",
|
|
||||||
"error.chain.configInvalid": "ไฟล์กำหนดค่าที่ {{path}} ไม่ถูกต้อง",
|
|
||||||
"error.chain.configInvalidWithMessage": "ไฟล์กำหนดค่าที่ {{path}} ไม่ถูกต้อง: {{message}}",
|
|
||||||
|
|
||||||
"notification.permission.title": "ต้องการสิทธิ์",
|
|
||||||
"notification.permission.description": "{{sessionTitle}} ใน {{projectName}} ต้องการสิทธิ์",
|
|
||||||
"notification.question.title": "คำถาม",
|
|
||||||
"notification.question.description": "{{sessionTitle}} ใน {{projectName}} มีคำถาม",
|
|
||||||
"notification.action.goToSession": "ไปที่เซสชัน",
|
|
||||||
|
|
||||||
"notification.session.responseReady.title": "การตอบสนองพร้อม",
|
|
||||||
"notification.session.error.title": "ข้อผิดพลาดเซสชัน",
|
|
||||||
"notification.session.error.fallbackDescription": "เกิดข้อผิดพลาด",
|
|
||||||
|
|
||||||
"home.recentProjects": "โปรเจกต์ล่าสุด",
|
|
||||||
"home.empty.title": "ไม่มีโปรเจกต์ล่าสุด",
|
|
||||||
"home.empty.description": "เริ่มต้นโดยเปิดโปรเจกต์ในเครื่อง",
|
|
||||||
|
|
||||||
"session.tab.session": "เซสชัน",
|
|
||||||
"session.tab.review": "ตรวจสอบ",
|
|
||||||
"session.tab.context": "บริบท",
|
|
||||||
"session.panel.reviewAndFiles": "ตรวจสอบและไฟล์",
|
|
||||||
"session.review.filesChanged": "{{count}} ไฟล์ที่เปลี่ยนแปลง",
|
|
||||||
"session.review.change.one": "การเปลี่ยนแปลง",
|
|
||||||
"session.review.change.other": "การเปลี่ยนแปลง",
|
|
||||||
"session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...",
|
|
||||||
"session.review.empty": "ยังไม่มีการเปลี่ยนแปลงในเซสชันนี้",
|
|
||||||
"session.review.noChanges": "ไม่มีการเปลี่ยนแปลง",
|
|
||||||
|
|
||||||
"session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด",
|
|
||||||
"session.files.all": "ไฟล์ทั้งหมด",
|
|
||||||
|
|
||||||
"session.messages.renderEarlier": "แสดงข้อความก่อนหน้า",
|
|
||||||
"session.messages.loadingEarlier": "กำลังโหลดข้อความก่อนหน้า...",
|
|
||||||
"session.messages.loadEarlier": "โหลดข้อความก่อนหน้า",
|
|
||||||
"session.messages.loading": "กำลังโหลดข้อความ...",
|
|
||||||
"session.messages.jumpToLatest": "ไปที่ล่าสุด",
|
|
||||||
|
|
||||||
"session.context.addToContext": "เพิ่ม {{selection}} ไปยังบริบท",
|
|
||||||
|
|
||||||
"session.new.worktree.main": "สาขาหลัก",
|
|
||||||
"session.new.worktree.mainWithBranch": "สาขาหลัก ({{branch}})",
|
|
||||||
"session.new.worktree.create": "สร้าง worktree ใหม่",
|
|
||||||
"session.new.lastModified": "แก้ไขล่าสุด",
|
|
||||||
|
|
||||||
"session.header.search.placeholder": "ค้นหา {{project}}",
|
|
||||||
"session.header.searchFiles": "ค้นหาไฟล์",
|
|
||||||
|
|
||||||
"status.popover.trigger": "สถานะ",
|
|
||||||
"status.popover.ariaLabel": "การกำหนดค่าเซิร์ฟเวอร์",
|
|
||||||
"status.popover.tab.servers": "เซิร์ฟเวอร์",
|
|
||||||
"status.popover.tab.mcp": "MCP",
|
|
||||||
"status.popover.tab.lsp": "LSP",
|
|
||||||
"status.popover.tab.plugins": "ปลั๊กอิน",
|
|
||||||
"status.popover.action.manageServers": "จัดการเซิร์ฟเวอร์",
|
|
||||||
|
|
||||||
"session.share.popover.title": "เผยแพร่บนเว็บ",
|
|
||||||
"session.share.popover.description.shared": "เซสชันนี้เป็นสาธารณะบนเว็บ สามารถเข้าถึงได้โดยผู้ที่มีลิงก์",
|
|
||||||
"session.share.popover.description.unshared": "แชร์เซสชันสาธารณะบนเว็บ จะเข้าถึงได้โดยผู้ที่มีลิงก์",
|
|
||||||
"session.share.action.share": "แชร์",
|
|
||||||
"session.share.action.publish": "เผยแพร่",
|
|
||||||
"session.share.action.publishing": "กำลังเผยแพร่...",
|
|
||||||
"session.share.action.unpublish": "ยกเลิกการเผยแพร่",
|
|
||||||
"session.share.action.unpublishing": "กำลังยกเลิกการเผยแพร่...",
|
|
||||||
"session.share.action.view": "ดู",
|
|
||||||
"session.share.copy.copied": "คัดลอกแล้ว",
|
|
||||||
"session.share.copy.copyLink": "คัดลอกลิงก์",
|
|
||||||
|
|
||||||
"lsp.tooltip.none": "ไม่มีเซิร์ฟเวอร์ LSP",
|
|
||||||
"lsp.label.connected": "{{count}} LSP",
|
|
||||||
|
|
||||||
"prompt.loading": "กำลังโหลดพร้อมท์...",
|
|
||||||
"terminal.loading": "กำลังโหลดเทอร์มินัล...",
|
|
||||||
"terminal.title": "เทอร์มินัล",
|
|
||||||
"terminal.title.numbered": "เทอร์มินัล {{number}}",
|
|
||||||
"terminal.close": "ปิดเทอร์มินัล",
|
|
||||||
"terminal.connectionLost.title": "การเชื่อมต่อขาดหาย",
|
|
||||||
"terminal.connectionLost.description": "การเชื่อมต่อเทอร์มินัลถูกขัดจังหวะ อาจเกิดขึ้นเมื่อเซิร์ฟเวอร์รีสตาร์ท",
|
|
||||||
|
|
||||||
"common.closeTab": "ปิดแท็บ",
|
|
||||||
"common.dismiss": "ปิด",
|
|
||||||
"common.requestFailed": "คำขอล้มเหลว",
|
|
||||||
"common.moreOptions": "ตัวเลือกเพิ่มเติม",
|
|
||||||
"common.learnMore": "เรียนรู้เพิ่มเติม",
|
|
||||||
"common.rename": "เปลี่ยนชื่อ",
|
|
||||||
"common.reset": "รีเซ็ต",
|
|
||||||
"common.archive": "จัดเก็บ",
|
|
||||||
"common.delete": "ลบ",
|
|
||||||
"common.close": "ปิด",
|
|
||||||
"common.edit": "แก้ไข",
|
|
||||||
"common.loadMore": "โหลดเพิ่มเติม",
|
|
||||||
"common.key.esc": "ESC",
|
|
||||||
|
|
||||||
"sidebar.menu.toggle": "สลับเมนู",
|
|
||||||
"sidebar.nav.projectsAndSessions": "โปรเจกต์และเซสชัน",
|
|
||||||
"sidebar.settings": "การตั้งค่า",
|
|
||||||
"sidebar.help": "ช่วยเหลือ",
|
|
||||||
"sidebar.workspaces.enable": "เปิดใช้งานพื้นที่ทำงาน",
|
|
||||||
"sidebar.workspaces.disable": "ปิดใช้งานพื้นที่ทำงาน",
|
|
||||||
"sidebar.gettingStarted.title": "เริ่มต้นใช้งาน",
|
|
||||||
"sidebar.gettingStarted.line1": "OpenCode รวมถึงโมเดลฟรีเพื่อให้คุณเริ่มต้นได้ทันที",
|
|
||||||
"sidebar.gettingStarted.line2": "เชื่อมต่อผู้ให้บริการใด ๆ เพื่อใช้โมเดล รวมถึง Claude, GPT, Gemini ฯลฯ",
|
|
||||||
"sidebar.project.recentSessions": "เซสชันล่าสุด",
|
|
||||||
"sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด",
|
|
||||||
|
|
||||||
"app.name.desktop": "OpenCode Desktop",
|
|
||||||
|
|
||||||
"settings.section.desktop": "เดสก์ท็อป",
|
|
||||||
"settings.section.server": "เซิร์ฟเวอร์",
|
|
||||||
"settings.tab.general": "ทั่วไป",
|
|
||||||
"settings.tab.shortcuts": "ทางลัด",
|
|
||||||
|
|
||||||
"settings.general.section.appearance": "รูปลักษณ์",
|
|
||||||
"settings.general.section.notifications": "การแจ้งเตือนระบบ",
|
|
||||||
"settings.general.section.updates": "การอัปเดต",
|
|
||||||
"settings.general.section.sounds": "เสียงเอฟเฟกต์",
|
|
||||||
|
|
||||||
"settings.general.row.language.title": "ภาษา",
|
|
||||||
"settings.general.row.language.description": "เปลี่ยนภาษาที่แสดงสำหรับ OpenCode",
|
|
||||||
"settings.general.row.appearance.title": "รูปลักษณ์",
|
|
||||||
"settings.general.row.appearance.description": "ปรับแต่งวิธีการที่ OpenCode มีลักษณะบนอุปกรณ์ของคุณ",
|
|
||||||
"settings.general.row.theme.title": "ธีม",
|
|
||||||
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม",
|
|
||||||
"settings.general.row.font.title": "ฟอนต์",
|
|
||||||
"settings.general.row.font.description": "ปรับแต่งฟอนต์โมโนที่ใช้ในบล็อกโค้ด",
|
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "บันทึกการอัปเดต",
|
|
||||||
"settings.general.row.releaseNotes.description": "แสดงป๊อปอัพ What's New หลังจากอัปเดต",
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
|
||||||
"font.option.firaCode": "Fira Code",
|
|
||||||
"font.option.hack": "Hack",
|
|
||||||
"font.option.inconsolata": "Inconsolata",
|
|
||||||
"font.option.intelOneMono": "Intel One Mono",
|
|
||||||
"font.option.iosevka": "Iosevka",
|
|
||||||
"font.option.jetbrainsMono": "JetBrains Mono",
|
|
||||||
"font.option.mesloLgs": "Meslo LGS",
|
|
||||||
"font.option.robotoMono": "Roboto Mono",
|
|
||||||
"font.option.sourceCodePro": "Source Code Pro",
|
|
||||||
"font.option.ubuntuMono": "Ubuntu Mono",
|
|
||||||
"sound.option.alert01": "เสียงเตือน 01",
|
|
||||||
"sound.option.alert02": "เสียงเตือน 02",
|
|
||||||
"sound.option.alert03": "เสียงเตือน 03",
|
|
||||||
"sound.option.alert04": "เสียงเตือน 04",
|
|
||||||
"sound.option.alert05": "เสียงเตือน 05",
|
|
||||||
"sound.option.alert06": "เสียงเตือน 06",
|
|
||||||
"sound.option.alert07": "เสียงเตือน 07",
|
|
||||||
"sound.option.alert08": "เสียงเตือน 08",
|
|
||||||
"sound.option.alert09": "เสียงเตือน 09",
|
|
||||||
"sound.option.alert10": "เสียงเตือน 10",
|
|
||||||
"sound.option.bipbop01": "Bip-bop 01",
|
|
||||||
"sound.option.bipbop02": "Bip-bop 02",
|
|
||||||
"sound.option.bipbop03": "Bip-bop 03",
|
|
||||||
"sound.option.bipbop04": "Bip-bop 04",
|
|
||||||
"sound.option.bipbop05": "Bip-bop 05",
|
|
||||||
"sound.option.bipbop06": "Bip-bop 06",
|
|
||||||
"sound.option.bipbop07": "Bip-bop 07",
|
|
||||||
"sound.option.bipbop08": "Bip-bop 08",
|
|
||||||
"sound.option.bipbop09": "Bip-bop 09",
|
|
||||||
"sound.option.bipbop10": "Bip-bop 10",
|
|
||||||
"sound.option.staplebops01": "Staplebops 01",
|
|
||||||
"sound.option.staplebops02": "Staplebops 02",
|
|
||||||
"sound.option.staplebops03": "Staplebops 03",
|
|
||||||
"sound.option.staplebops04": "Staplebops 04",
|
|
||||||
"sound.option.staplebops05": "Staplebops 05",
|
|
||||||
"sound.option.staplebops06": "Staplebops 06",
|
|
||||||
"sound.option.staplebops07": "Staplebops 07",
|
|
||||||
"sound.option.nope01": "Nope 01",
|
|
||||||
"sound.option.nope02": "Nope 02",
|
|
||||||
"sound.option.nope03": "Nope 03",
|
|
||||||
"sound.option.nope04": "Nope 04",
|
|
||||||
"sound.option.nope05": "Nope 05",
|
|
||||||
"sound.option.nope06": "Nope 06",
|
|
||||||
"sound.option.nope07": "Nope 07",
|
|
||||||
"sound.option.nope08": "Nope 08",
|
|
||||||
"sound.option.nope09": "Nope 09",
|
|
||||||
"sound.option.nope10": "Nope 10",
|
|
||||||
"sound.option.nope11": "Nope 11",
|
|
||||||
"sound.option.nope12": "Nope 12",
|
|
||||||
"sound.option.yup01": "Yup 01",
|
|
||||||
"sound.option.yup02": "Yup 02",
|
|
||||||
"sound.option.yup03": "Yup 03",
|
|
||||||
"sound.option.yup04": "Yup 04",
|
|
||||||
"sound.option.yup05": "Yup 05",
|
|
||||||
"sound.option.yup06": "Yup 06",
|
|
||||||
|
|
||||||
"settings.general.notifications.agent.title": "เอเจนต์",
|
|
||||||
"settings.general.notifications.agent.description": "แสดงการแจ้งเตือนระบบเมื่อเอเจนต์เสร็จสิ้นหรือต้องการความสนใจ",
|
|
||||||
"settings.general.notifications.permissions.title": "สิทธิ์",
|
|
||||||
"settings.general.notifications.permissions.description": "แสดงการแจ้งเตือนระบบเมื่อต้องการสิทธิ์",
|
|
||||||
"settings.general.notifications.errors.title": "ข้อผิดพลาด",
|
|
||||||
"settings.general.notifications.errors.description": "แสดงการแจ้งเตือนระบบเมื่อเกิดข้อผิดพลาด",
|
|
||||||
|
|
||||||
"settings.general.sounds.agent.title": "เอเจนต์",
|
|
||||||
"settings.general.sounds.agent.description": "เล่นเสียงเมื่อเอเจนต์เสร็จสิ้นหรือต้องการความสนใจ",
|
|
||||||
"settings.general.sounds.permissions.title": "สิทธิ์",
|
|
||||||
"settings.general.sounds.permissions.description": "เล่นเสียงเมื่อต้องการสิทธิ์",
|
|
||||||
"settings.general.sounds.errors.title": "ข้อผิดพลาด",
|
|
||||||
"settings.general.sounds.errors.description": "เล่นเสียงเมื่อเกิดข้อผิดพลาด",
|
|
||||||
|
|
||||||
"settings.shortcuts.title": "ทางลัดแป้นพิมพ์",
|
|
||||||
"settings.shortcuts.reset.button": "รีเซ็ตเป็นค่าเริ่มต้น",
|
|
||||||
"settings.shortcuts.reset.toast.title": "รีเซ็ตทางลัดแล้ว",
|
|
||||||
"settings.shortcuts.reset.toast.description": "รีเซ็ตทางลัดแป้นพิมพ์เป็นค่าเริ่มต้นแล้ว",
|
|
||||||
"settings.shortcuts.conflict.title": "ทางลัดใช้งานอยู่แล้ว",
|
|
||||||
"settings.shortcuts.conflict.description": "{{keybind}} ถูกกำหนดให้กับ {{titles}} แล้ว",
|
|
||||||
"settings.shortcuts.unassigned": "ไม่ได้กำหนด",
|
|
||||||
"settings.shortcuts.pressKeys": "กดปุ่ม",
|
|
||||||
"settings.shortcuts.search.placeholder": "ค้นหาทางลัด",
|
|
||||||
"settings.shortcuts.search.empty": "ไม่พบทางลัด",
|
|
||||||
|
|
||||||
"settings.shortcuts.group.general": "ทั่วไป",
|
|
||||||
"settings.shortcuts.group.session": "เซสชัน",
|
|
||||||
"settings.shortcuts.group.navigation": "การนำทาง",
|
|
||||||
"settings.shortcuts.group.modelAndAgent": "โมเดลและเอเจนต์",
|
|
||||||
"settings.shortcuts.group.terminal": "เทอร์มินัล",
|
|
||||||
"settings.shortcuts.group.prompt": "พร้อมท์",
|
|
||||||
|
|
||||||
"settings.providers.title": "ผู้ให้บริการ",
|
|
||||||
"settings.providers.description": "การตั้งค่าผู้ให้บริการจะสามารถกำหนดค่าได้ที่นี่",
|
|
||||||
"settings.providers.section.connected": "ผู้ให้บริการที่เชื่อมต่อ",
|
|
||||||
"settings.providers.connected.empty": "ไม่มีผู้ให้บริการที่เชื่อมต่อ",
|
|
||||||
"settings.providers.section.popular": "ผู้ให้บริการยอดนิยม",
|
|
||||||
"settings.providers.tag.environment": "สภาพแวดล้อม",
|
|
||||||
"settings.providers.tag.config": "กำหนดค่า",
|
|
||||||
"settings.providers.tag.custom": "กำหนดเอง",
|
|
||||||
"settings.providers.tag.other": "อื่น ๆ",
|
|
||||||
"settings.models.title": "โมเดล",
|
|
||||||
"settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่",
|
|
||||||
"settings.agents.title": "เอเจนต์",
|
|
||||||
"settings.agents.description": "การตั้งค่าเอเจนต์จะสามารถกำหนดค่าได้ที่นี่",
|
|
||||||
"settings.commands.title": "คำสั่ง",
|
|
||||||
"settings.commands.description": "การตั้งค่าคำสั่งจะสามารถกำหนดค่าได้ที่นี่",
|
|
||||||
"settings.mcp.title": "MCP",
|
|
||||||
"settings.mcp.description": "การตั้งค่า MCP จะสามารถกำหนดค่าได้ที่นี่",
|
|
||||||
|
|
||||||
"settings.permissions.title": "สิทธิ์",
|
|
||||||
"settings.permissions.description": "ควบคุมเครื่องมือที่เซิร์ฟเวอร์สามารถใช้โดยค่าเริ่มต้น",
|
|
||||||
"settings.permissions.section.tools": "เครื่องมือ",
|
|
||||||
"settings.permissions.toast.updateFailed.title": "ไม่สามารถอัปเดตสิทธิ์",
|
|
||||||
|
|
||||||
"settings.permissions.action.allow": "อนุญาต",
|
|
||||||
"settings.permissions.action.ask": "ถาม",
|
|
||||||
"settings.permissions.action.deny": "ปฏิเสธ",
|
|
||||||
|
|
||||||
"settings.permissions.tool.read.title": "อ่าน",
|
|
||||||
"settings.permissions.tool.read.description": "อ่านไฟล์ (ตรงกับเส้นทางไฟล์)",
|
|
||||||
"settings.permissions.tool.edit.title": "แก้ไข",
|
|
||||||
"settings.permissions.tool.edit.description": "แก้ไขไฟล์ รวมถึงการแก้ไข เขียน แพตช์ และแก้ไขหลายรายการ",
|
|
||||||
"settings.permissions.tool.glob.title": "Glob",
|
|
||||||
"settings.permissions.tool.glob.description": "จับคู่ไฟล์โดยใช้รูปแบบ glob",
|
|
||||||
"settings.permissions.tool.grep.title": "Grep",
|
|
||||||
"settings.permissions.tool.grep.description": "ค้นหาเนื้อหาไฟล์โดยใช้นิพจน์ทั่วไป",
|
|
||||||
"settings.permissions.tool.list.title": "รายการ",
|
|
||||||
"settings.permissions.tool.list.description": "แสดงรายการไฟล์ภายในไดเรกทอรี",
|
|
||||||
"settings.permissions.tool.bash.title": "Bash",
|
|
||||||
"settings.permissions.tool.bash.description": "เรียกใช้คำสั่งเชลล์",
|
|
||||||
"settings.permissions.tool.task.title": "งาน",
|
|
||||||
"settings.permissions.tool.task.description": "เปิดเอเจนต์ย่อย",
|
|
||||||
"settings.permissions.tool.skill.title": "ทักษะ",
|
|
||||||
"settings.permissions.tool.skill.description": "โหลดทักษะตามชื่อ",
|
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
|
||||||
"settings.permissions.tool.lsp.description": "เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษา",
|
|
||||||
"settings.permissions.tool.todoread.title": "อ่านรายการงาน",
|
|
||||||
"settings.permissions.tool.todoread.description": "อ่านรายการงาน",
|
|
||||||
"settings.permissions.tool.todowrite.title": "เขียนรายการงาน",
|
|
||||||
"settings.permissions.tool.todowrite.description": "อัปเดตรายการงาน",
|
|
||||||
"settings.permissions.tool.webfetch.title": "ดึงข้อมูลจากเว็บ",
|
|
||||||
"settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL",
|
|
||||||
"settings.permissions.tool.websearch.title": "ค้นหาเว็บ",
|
|
||||||
"settings.permissions.tool.websearch.description": "ค้นหาบนเว็บ",
|
|
||||||
"settings.permissions.tool.codesearch.title": "ค้นหาโค้ด",
|
|
||||||
"settings.permissions.tool.codesearch.description": "ค้นหาโค้ดบนเว็บ",
|
|
||||||
"settings.permissions.tool.external_directory.title": "ไดเรกทอรีภายนอก",
|
|
||||||
"settings.permissions.tool.external_directory.description": "เข้าถึงไฟล์นอกไดเรกทอรีโปรเจกต์",
|
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
|
||||||
"settings.permissions.tool.doom_loop.description": "ตรวจจับการเรียกเครื่องมือซ้ำด้วยข้อมูลนำเข้าเหมือนกัน",
|
|
||||||
|
|
||||||
"session.delete.failed.title": "ไม่สามารถลบเซสชัน",
|
|
||||||
"session.delete.title": "ลบเซสชัน",
|
|
||||||
"session.delete.confirm": 'ลบเซสชัน "{{name}}" หรือไม่?',
|
|
||||||
"session.delete.button": "ลบเซสชัน",
|
|
||||||
|
|
||||||
"workspace.new": "พื้นที่ทำงานใหม่",
|
|
||||||
"workspace.type.local": "ในเครื่อง",
|
|
||||||
"workspace.type.sandbox": "แซนด์บ็อกซ์",
|
|
||||||
"workspace.create.failed.title": "ไม่สามารถสร้างพื้นที่ทำงาน",
|
|
||||||
"workspace.delete.failed.title": "ไม่สามารถลบพื้นที่ทำงาน",
|
|
||||||
"workspace.resetting.title": "กำลังรีเซ็ตพื้นที่ทำงาน",
|
|
||||||
"workspace.resetting.description": "อาจใช้เวลาประมาณหนึ่งนาที",
|
|
||||||
"workspace.reset.failed.title": "ไม่สามารถรีเซ็ตพื้นที่ทำงาน",
|
|
||||||
"workspace.reset.success.title": "รีเซ็ตพื้นที่ทำงานแล้ว",
|
|
||||||
"workspace.reset.success.description": "พื้นที่ทำงานตรงกับสาขาเริ่มต้นแล้ว",
|
|
||||||
"workspace.error.stillPreparing": "พื้นที่ทำงานกำลังเตรียมอยู่",
|
|
||||||
"workspace.status.checking": "กำลังตรวจสอบการเปลี่ยนแปลงที่ไม่ได้ผสาน...",
|
|
||||||
"workspace.status.error": "ไม่สามารถตรวจสอบสถานะ git",
|
|
||||||
"workspace.status.clean": "ไม่ตรวจพบการเปลี่ยนแปลงที่ไม่ได้ผสาน",
|
|
||||||
"workspace.status.dirty": "ตรวจพบการเปลี่ยนแปลงที่ไม่ได้ผสานในพื้นที่ทำงานนี้",
|
|
||||||
"workspace.delete.title": "ลบพื้นที่ทำงาน",
|
|
||||||
"workspace.delete.confirm": 'ลบพื้นที่ทำงาน "{{name}}" หรือไม่?',
|
|
||||||
"workspace.delete.button": "ลบพื้นที่ทำงาน",
|
|
||||||
"workspace.reset.title": "รีเซ็ตพื้นที่ทำงาน",
|
|
||||||
"workspace.reset.confirm": 'รีเซ็ตพื้นที่ทำงาน "{{name}}" หรือไม่?',
|
|
||||||
"workspace.reset.button": "รีเซ็ตพื้นที่ทำงาน",
|
|
||||||
"workspace.reset.archived.none": "ไม่มีเซสชันที่ใช้งานอยู่จะถูกจัดเก็บ",
|
|
||||||
"workspace.reset.archived.one": "1 เซสชันจะถูกจัดเก็บ",
|
|
||||||
"workspace.reset.archived.many": "{{count}} เซสชันจะถูกจัดเก็บ",
|
|
||||||
"workspace.reset.note": "สิ่งนี้จะรีเซ็ตพื้นที่ทำงานให้ตรงกับสาขาเริ่มต้น",
|
|
||||||
}
|
|
||||||
+27
-44
@@ -37,12 +37,12 @@ export const dict = {
|
|||||||
"command.palette": "命令面板",
|
"command.palette": "命令面板",
|
||||||
|
|
||||||
"command.theme.cycle": "切换主题",
|
"command.theme.cycle": "切换主题",
|
||||||
"command.theme.set": "使用主题:{{theme}}",
|
"command.theme.set": "使用主题: {{theme}}",
|
||||||
"command.theme.scheme.cycle": "切换配色方案",
|
"command.theme.scheme.cycle": "切换配色方案",
|
||||||
"command.theme.scheme.set": "使用配色方案:{{scheme}}",
|
"command.theme.scheme.set": "使用配色方案: {{scheme}}",
|
||||||
|
|
||||||
"command.language.cycle": "切换语言",
|
"command.language.cycle": "切换语言",
|
||||||
"command.language.set": "使用语言:{{language}}",
|
"command.language.set": "使用语言: {{language}}",
|
||||||
|
|
||||||
"command.session.new": "新建会话",
|
"command.session.new": "新建会话",
|
||||||
"command.file.open": "打开文件",
|
"command.file.open": "打开文件",
|
||||||
@@ -98,10 +98,6 @@ export const dict = {
|
|||||||
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
|
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
|
||||||
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
|
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
|
||||||
"dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接",
|
"dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接",
|
||||||
"dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接",
|
|
||||||
"dialog.provider.google.note": "使用 Google 账号或 API 密钥连接",
|
|
||||||
"dialog.provider.openrouter.note": "使用 OpenRouter 账号或 API 密钥连接",
|
|
||||||
"dialog.provider.vercel.note": "使用 Vercel 账号或 API 密钥连接",
|
|
||||||
|
|
||||||
"dialog.model.select.title": "选择模型",
|
"dialog.model.select.title": "选择模型",
|
||||||
"dialog.model.search.placeholder": "搜索模型",
|
"dialog.model.search.placeholder": "搜索模型",
|
||||||
@@ -120,7 +116,7 @@ export const dict = {
|
|||||||
"provider.connect.method.apiKey": "API 密钥",
|
"provider.connect.method.apiKey": "API 密钥",
|
||||||
"provider.connect.status.inProgress": "正在授权...",
|
"provider.connect.status.inProgress": "正在授权...",
|
||||||
"provider.connect.status.waiting": "等待授权...",
|
"provider.connect.status.waiting": "等待授权...",
|
||||||
"provider.connect.status.failed": "授权失败:{{error}}",
|
"provider.connect.status.failed": "授权失败: {{error}}",
|
||||||
"provider.connect.apiKey.description":
|
"provider.connect.apiKey.description":
|
||||||
"输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。",
|
"输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。",
|
||||||
"provider.connect.apiKey.label": "{{provider}} API 密钥",
|
"provider.connect.apiKey.label": "{{provider}} API 密钥",
|
||||||
@@ -160,7 +156,7 @@ export const dict = {
|
|||||||
"model.input.audio": "音频",
|
"model.input.audio": "音频",
|
||||||
"model.input.video": "视频",
|
"model.input.video": "视频",
|
||||||
"model.input.pdf": "pdf",
|
"model.input.pdf": "pdf",
|
||||||
"model.tooltip.allows": "支持:{{inputs}}",
|
"model.tooltip.allows": "支持: {{inputs}}",
|
||||||
"model.tooltip.reasoning.allowed": "支持推理",
|
"model.tooltip.reasoning.allowed": "支持推理",
|
||||||
"model.tooltip.reasoning.none": "不支持推理",
|
"model.tooltip.reasoning.none": "不支持推理",
|
||||||
"model.tooltip.context": "上下文上限 {{limit}}",
|
"model.tooltip.context": "上下文上限 {{limit}}",
|
||||||
@@ -185,30 +181,30 @@ export const dict = {
|
|||||||
"prompt.mode.shell.exit": "按 esc 退出",
|
"prompt.mode.shell.exit": "按 esc 退出",
|
||||||
|
|
||||||
"prompt.example.1": "修复代码库中的一个 TODO",
|
"prompt.example.1": "修复代码库中的一个 TODO",
|
||||||
"prompt.example.2": "这个项目的技术栈是什么?",
|
"prompt.example.2": "这个项目的技术栈是什么?",
|
||||||
"prompt.example.3": "修复失败的测试",
|
"prompt.example.3": "修复失败的测试",
|
||||||
"prompt.example.4": "解释认证是如何工作的",
|
"prompt.example.4": "解释认证是如何工作的",
|
||||||
"prompt.example.5": "查找并修复安全漏洞",
|
"prompt.example.5": "查找并修复安全漏洞",
|
||||||
"prompt.example.6": "为用户服务添加单元测试",
|
"prompt.example.6": "为用户服务添加单元测试",
|
||||||
"prompt.example.7": "重构这个函数,让它更易读",
|
"prompt.example.7": "重构这个函数,让它更易读",
|
||||||
"prompt.example.8": "这个错误是什么意思?",
|
"prompt.example.8": "这个错误是什么意思?",
|
||||||
"prompt.example.9": "帮我调试这个问题",
|
"prompt.example.9": "帮我调试这个问题",
|
||||||
"prompt.example.10": "生成 API 文档",
|
"prompt.example.10": "生成 API 文档",
|
||||||
"prompt.example.11": "优化数据库查询",
|
"prompt.example.11": "优化数据库查询",
|
||||||
"prompt.example.12": "添加输入校验",
|
"prompt.example.12": "添加输入校验",
|
||||||
"prompt.example.13": "创建一个新的组件用于...",
|
"prompt.example.13": "创建一个新的组件用于...",
|
||||||
"prompt.example.14": "我该如何部署这个项目?",
|
"prompt.example.14": "我该如何部署这个项目?",
|
||||||
"prompt.example.15": "审查我的代码并给出最佳实践建议",
|
"prompt.example.15": "审查我的代码并给出最佳实践建议",
|
||||||
"prompt.example.16": "为这个函数添加错误处理",
|
"prompt.example.16": "为这个函数添加错误处理",
|
||||||
"prompt.example.17": "解释这个正则表达式",
|
"prompt.example.17": "解释这个正则表达式",
|
||||||
"prompt.example.18": "把它转换成 TypeScript",
|
"prompt.example.18": "把它转换成 TypeScript",
|
||||||
"prompt.example.19": "在整个代码库中添加日志",
|
"prompt.example.19": "在整个代码库中添加日志",
|
||||||
"prompt.example.20": "哪些依赖已经过期?",
|
"prompt.example.20": "哪些依赖已经过期?",
|
||||||
"prompt.example.21": "帮我写一个迁移脚本",
|
"prompt.example.21": "帮我写一个迁移脚本",
|
||||||
"prompt.example.22": "为这个接口实现缓存",
|
"prompt.example.22": "为这个接口实现缓存",
|
||||||
"prompt.example.23": "给这个列表添加分页",
|
"prompt.example.23": "给这个列表添加分页",
|
||||||
"prompt.example.24": "创建一个 CLI 命令用于...",
|
"prompt.example.24": "创建一个 CLI 命令用于...",
|
||||||
"prompt.example.25": "这里的环境变量是怎么工作的?",
|
"prompt.example.25": "这里的环境变量是怎么工作的?",
|
||||||
|
|
||||||
"prompt.popover.emptyResults": "没有匹配的结果",
|
"prompt.popover.emptyResults": "没有匹配的结果",
|
||||||
"prompt.popover.emptyCommands": "没有匹配的命令",
|
"prompt.popover.emptyCommands": "没有匹配的命令",
|
||||||
@@ -334,7 +330,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "语言",
|
"toast.language.title": "语言",
|
||||||
"toast.language.description": "已切换到{{language}}",
|
"toast.language.description": "已切换到{{language}}",
|
||||||
@@ -382,31 +377,31 @@ export const dict = {
|
|||||||
"error.page.action.updateTo": "更新到 {{version}}",
|
"error.page.action.updateTo": "更新到 {{version}}",
|
||||||
"error.page.report.prefix": "请将此错误报告给 OpenCode 团队",
|
"error.page.report.prefix": "请将此错误报告给 OpenCode 团队",
|
||||||
"error.page.report.discord": "在 Discord 上",
|
"error.page.report.discord": "在 Discord 上",
|
||||||
"error.page.version": "版本:{{version}}",
|
"error.page.version": "版本: {{version}}",
|
||||||
|
|
||||||
"error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?",
|
"error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html? 或者 id 属性拼写错了?",
|
||||||
|
|
||||||
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
|
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
|
||||||
|
|
||||||
"error.chain.unknown": "未知错误",
|
"error.chain.unknown": "未知错误",
|
||||||
"error.chain.causedBy": "原因:",
|
"error.chain.causedBy": "原因:",
|
||||||
"error.chain.apiError": "API 错误",
|
"error.chain.apiError": "API 错误",
|
||||||
"error.chain.status": "状态:{{status}}",
|
"error.chain.status": "状态: {{status}}",
|
||||||
"error.chain.retryable": "可重试:{{retryable}}",
|
"error.chain.retryable": "可重试: {{retryable}}",
|
||||||
"error.chain.responseBody": "响应内容:\n{{body}}",
|
"error.chain.responseBody": "响应内容:\n{{body}}",
|
||||||
"error.chain.didYouMean": "你是不是想输入:{{suggestions}}",
|
"error.chain.didYouMean": "你是不是想输入: {{suggestions}}",
|
||||||
"error.chain.modelNotFound": "未找到模型:{{provider}}/{{model}}",
|
"error.chain.modelNotFound": "未找到模型: {{provider}}/{{model}}",
|
||||||
"error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称",
|
"error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称",
|
||||||
"error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意: OpenCode 暂不支持 MCP 认证。',
|
"error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意: OpenCode 暂不支持 MCP 认证。',
|
||||||
"error.chain.providerAuthFailed": "提供商认证失败({{provider}}):{{message}}",
|
"error.chain.providerAuthFailed": "提供商认证失败 ({{provider}}): {{message}}",
|
||||||
"error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。',
|
"error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。',
|
||||||
"error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)",
|
"error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)",
|
||||||
"error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C):{{message}}",
|
"error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C): {{message}}",
|
||||||
"error.chain.configDirectoryTypo":
|
"error.chain.configDirectoryTypo":
|
||||||
'{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。',
|
'{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。',
|
||||||
"error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}",
|
"error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}",
|
||||||
"error.chain.configInvalid": "配置文件 {{path}} 无效",
|
"error.chain.configInvalid": "配置文件 {{path}} 无效",
|
||||||
"error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效:{{message}}",
|
"error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效: {{message}}",
|
||||||
|
|
||||||
"notification.permission.title": "需要权限",
|
"notification.permission.title": "需要权限",
|
||||||
"notification.permission.description": "{{sessionTitle}}({{projectName}})需要权限",
|
"notification.permission.description": "{{sessionTitle}}({{projectName}})需要权限",
|
||||||
@@ -443,7 +438,7 @@ export const dict = {
|
|||||||
"session.context.addToContext": "将 {{selection}} 添加到上下文",
|
"session.context.addToContext": "将 {{selection}} 添加到上下文",
|
||||||
|
|
||||||
"session.new.worktree.main": "主分支",
|
"session.new.worktree.main": "主分支",
|
||||||
"session.new.worktree.mainWithBranch": "主分支({{branch}})",
|
"session.new.worktree.mainWithBranch": "主分支 ({{branch}})",
|
||||||
"session.new.worktree.create": "创建新的 worktree",
|
"session.new.worktree.create": "创建新的 worktree",
|
||||||
"session.new.lastModified": "最后修改",
|
"session.new.lastModified": "最后修改",
|
||||||
|
|
||||||
@@ -515,7 +510,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "外观",
|
"settings.general.section.appearance": "外观",
|
||||||
"settings.general.section.notifications": "系统通知",
|
"settings.general.section.notifications": "系统通知",
|
||||||
"settings.general.section.updates": "更新",
|
|
||||||
"settings.general.section.sounds": "音效",
|
"settings.general.section.sounds": "音效",
|
||||||
|
|
||||||
"settings.general.row.language.title": "语言",
|
"settings.general.row.language.title": "语言",
|
||||||
@@ -526,17 +520,6 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "自定义 OpenCode 的主题。",
|
"settings.general.row.theme.description": "自定义 OpenCode 的主题。",
|
||||||
"settings.general.row.font.title": "字体",
|
"settings.general.row.font.title": "字体",
|
||||||
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
|
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
|
||||||
"settings.general.row.releaseNotes.title": "发行说明",
|
|
||||||
"settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗",
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "启动时检查更新",
|
|
||||||
"settings.updates.row.startup.description": "在 OpenCode 启动时自动检查更新",
|
|
||||||
"settings.updates.row.check.title": "检查更新",
|
|
||||||
"settings.updates.row.check.description": "手动检查更新并在有更新时安装",
|
|
||||||
"settings.updates.action.checkNow": "立即检查",
|
|
||||||
"settings.updates.action.checking": "正在检查...",
|
|
||||||
"settings.updates.toast.latest.title": "已是最新版本",
|
|
||||||
"settings.updates.toast.latest.description": "你正在使用最新版本的 OpenCode。",
|
|
||||||
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
@@ -689,7 +672,7 @@ export const dict = {
|
|||||||
|
|
||||||
"session.delete.failed.title": "删除会话失败",
|
"session.delete.failed.title": "删除会话失败",
|
||||||
"session.delete.title": "删除会话",
|
"session.delete.title": "删除会话",
|
||||||
"session.delete.confirm": '删除会话 "{{name}}"?',
|
"session.delete.confirm": '删除会话 "{{name}}"?',
|
||||||
"session.delete.button": "删除会话",
|
"session.delete.button": "删除会话",
|
||||||
|
|
||||||
"workspace.new": "新建工作区",
|
"workspace.new": "新建工作区",
|
||||||
@@ -708,10 +691,10 @@ export const dict = {
|
|||||||
"workspace.status.clean": "未检测到未合并的更改。",
|
"workspace.status.clean": "未检测到未合并的更改。",
|
||||||
"workspace.status.dirty": "检测到未合并的更改。",
|
"workspace.status.dirty": "检测到未合并的更改。",
|
||||||
"workspace.delete.title": "删除工作区",
|
"workspace.delete.title": "删除工作区",
|
||||||
"workspace.delete.confirm": '删除工作区 "{{name}}"?',
|
"workspace.delete.confirm": '删除工作区 "{{name}}"?',
|
||||||
"workspace.delete.button": "删除工作区",
|
"workspace.delete.button": "删除工作区",
|
||||||
"workspace.reset.title": "重置工作区",
|
"workspace.reset.title": "重置工作区",
|
||||||
"workspace.reset.confirm": '重置工作区 "{{name}}"?',
|
"workspace.reset.confirm": '重置工作区 "{{name}}"?',
|
||||||
"workspace.reset.button": "重置工作区",
|
"workspace.reset.button": "重置工作区",
|
||||||
"workspace.reset.archived.none": "不会归档任何活跃会话。",
|
"workspace.reset.archived.none": "不会归档任何活跃会话。",
|
||||||
"workspace.reset.archived.one": "将归档 1 个会话。",
|
"workspace.reset.archived.one": "将归档 1 个会话。",
|
||||||
|
|||||||
@@ -331,7 +331,6 @@ export const dict = {
|
|||||||
"language.ar": "العربية",
|
"language.ar": "العربية",
|
||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.th": "ไทย",
|
|
||||||
|
|
||||||
"toast.language.title": "語言",
|
"toast.language.title": "語言",
|
||||||
"toast.language.description": "已切換到 {{language}}",
|
"toast.language.description": "已切換到 {{language}}",
|
||||||
@@ -512,7 +511,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "外觀",
|
"settings.general.section.appearance": "外觀",
|
||||||
"settings.general.section.notifications": "系統通知",
|
"settings.general.section.notifications": "系統通知",
|
||||||
"settings.general.section.updates": "更新",
|
|
||||||
"settings.general.section.sounds": "音效",
|
"settings.general.section.sounds": "音效",
|
||||||
|
|
||||||
"settings.general.row.language.title": "語言",
|
"settings.general.row.language.title": "語言",
|
||||||
@@ -524,18 +522,6 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "字型",
|
"settings.general.row.font.title": "字型",
|
||||||
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
|
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
|
||||||
|
|
||||||
"settings.general.row.releaseNotes.title": "發行說明",
|
|
||||||
"settings.general.row.releaseNotes.description": "更新後顯示「新功能」彈出視窗",
|
|
||||||
|
|
||||||
"settings.updates.row.startup.title": "啟動時檢查更新",
|
|
||||||
"settings.updates.row.startup.description": "在 OpenCode 啟動時自動檢查更新",
|
|
||||||
"settings.updates.row.check.title": "檢查更新",
|
|
||||||
"settings.updates.row.check.description": "手動檢查更新並在有更新時安裝",
|
|
||||||
"settings.updates.action.checkNow": "立即檢查",
|
|
||||||
"settings.updates.action.checking": "檢查中...",
|
|
||||||
"settings.updates.toast.latest.title": "已是最新版本",
|
|
||||||
"settings.updates.toast.latest.description": "你正在使用最新版本的 OpenCode。",
|
|
||||||
|
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
|
|||||||
@@ -1 +1,84 @@
|
|||||||
@import "@opencode-ai/ui/styles/tailwind";
|
@import "@opencode-ai/ui/styles/tailwind";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
a {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="markdown"] ul {
|
||||||
|
list-style: disc outside;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="markdown"] ol {
|
||||||
|
list-style: decimal outside;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="markdown"] li > p:first-child {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="markdown"] li > p + p {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
*[data-tauri-drag-region] {
|
||||||
|
app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-scroller::-webkit-scrollbar {
|
||||||
|
width: 10px !important;
|
||||||
|
height: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-scroller::-webkit-scrollbar-track {
|
||||||
|
background: transparent !important;
|
||||||
|
border-radius: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-scroller::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-weak-base) !important;
|
||||||
|
border-radius: 5px !important;
|
||||||
|
border: 3px solid transparent !important;
|
||||||
|
background-clip: padding-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-scroller::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--border-weak-base) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-scroller {
|
||||||
|
scrollbar-width: thin !important;
|
||||||
|
scrollbar-color: var(--border-weak-base) transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wider dialog variant for release notes modal */
|
||||||
|
[data-component="dialog"]:has(.dialog-release-notes) {
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
[data-slot="dialog-container"] {
|
||||||
|
width: min(100%, 720px);
|
||||||
|
height: min(100%, 400px);
|
||||||
|
margin-top: -80px;
|
||||||
|
|
||||||
|
[data-slot="dialog-content"] {
|
||||||
|
min-height: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
box-shadow: var(--shadow-lg-border-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="dialog-body"] {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,36 +1,22 @@
|
|||||||
import { createEffect, createMemo, Show, type ParentProps } from "solid-js"
|
import { createMemo, Show, type ParentProps } from "solid-js"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||||
import { SyncProvider, useSync } from "@/context/sync"
|
import { SyncProvider, useSync } from "@/context/sync"
|
||||||
import { LocalProvider } from "@/context/local"
|
import { LocalProvider } from "@/context/local"
|
||||||
|
|
||||||
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
|
import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
|
||||||
import { decode64 } from "@/utils/base64"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const language = useLanguage()
|
|
||||||
const directory = createMemo(() => {
|
const directory = createMemo(() => {
|
||||||
return decode64(params.dir) ?? ""
|
return base64Decode(params.dir!)
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!params.dir) return
|
|
||||||
if (directory()) return
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: "Invalid directory in URL.",
|
|
||||||
})
|
|
||||||
navigate("/")
|
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Show when={directory()}>
|
<Show when={params.dir}>
|
||||||
<SDKProvider directory={directory()}>
|
<SDKProvider directory={directory()}>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
{iife(() => {
|
{iife(() => {
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import { A, useNavigate, useParams } from "@solidjs/router"
|
|||||||
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
|
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
|
||||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
@@ -58,7 +57,6 @@ import { Binary } from "@opencode-ai/util/binary"
|
|||||||
import { retry } from "@opencode-ai/util/retry"
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { playSound, soundSrc } from "@/utils/sound"
|
import { playSound, soundSrc } from "@/utils/sound"
|
||||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||||
import { agentColor } from "@/utils/agent"
|
|
||||||
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||||
@@ -332,7 +330,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!platform.checkUpdate || !platform.update || !platform.restart) return
|
if (!platform.checkUpdate || !platform.update || !platform.restart) return
|
||||||
|
|
||||||
let toastId: number | undefined
|
let toastId: number | undefined
|
||||||
let interval: ReturnType<typeof setInterval> | undefined
|
|
||||||
|
|
||||||
async function pollUpdate() {
|
async function pollUpdate() {
|
||||||
const { updateAvailable, version } = await platform.checkUpdate!()
|
const { updateAvailable, version } = await platform.checkUpdate!()
|
||||||
@@ -359,25 +356,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
pollUpdate()
|
||||||
if (!settings.ready()) return
|
const interval = setInterval(pollUpdate, 10 * 60 * 1000)
|
||||||
|
onCleanup(() => clearInterval(interval))
|
||||||
if (!settings.updates.startup()) {
|
|
||||||
if (interval === undefined) return
|
|
||||||
clearInterval(interval)
|
|
||||||
interval = undefined
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interval !== undefined) return
|
|
||||||
void pollUpdate()
|
|
||||||
interval = setInterval(pollUpdate, 10 * 60 * 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (interval === undefined) return
|
|
||||||
clearInterval(interval)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -438,7 +419,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentDir = decode64(params.dir)
|
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||||
const currentSession = params.id
|
const currentSession = params.id
|
||||||
if (directory === currentDir && props.sessionID === currentSession) return
|
if (directory === currentDir && props.sessionID === currentSession) return
|
||||||
if (directory === currentDir && session?.parentID === currentSession) return
|
if (directory === currentDir && session?.parentID === currentSession) return
|
||||||
@@ -467,7 +448,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
onCleanup(unsub)
|
onCleanup(unsub)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const currentDir = decode64(params.dir)
|
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||||
const currentSession = params.id
|
const currentSession = params.id
|
||||||
if (!currentDir || !currentSession) return
|
if (!currentDir || !currentSession) return
|
||||||
const sessionKey = `${currentDir}:${currentSession}`
|
const sessionKey = `${currentDir}:${currentSession}`
|
||||||
@@ -521,7 +502,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentProject = createMemo(() => {
|
const currentProject = createMemo(() => {
|
||||||
const directory = decode64(params.dir)
|
const directory = params.dir ? base64Decode(params.dir) : undefined
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
|
|
||||||
const projects = layout.projects.list()
|
const projects = layout.projects.list()
|
||||||
@@ -576,6 +557,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
openProject(next.worktree, false)
|
openProject(next.worktree, false)
|
||||||
navigateToProject(next.worktree)
|
navigateToProject(next.worktree)
|
||||||
},
|
},
|
||||||
|
{ defer: true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -655,7 +637,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const compare = sortSessions(Date.now())
|
const compare = sortSessions(Date.now())
|
||||||
if (workspaceSetting()) {
|
if (workspaceSetting()) {
|
||||||
const dirs = workspaceIds(project)
|
const dirs = workspaceIds(project)
|
||||||
const activeDir = decode64(params.dir) ?? ""
|
const activeDir = params.dir ? base64Decode(params.dir) : ""
|
||||||
const result: Session[] = []
|
const result: Session[] = []
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
const expanded = store.workspaceExpanded[dir] ?? dir === project.worktree
|
const expanded = store.workspaceExpanded[dir] ?? dir === project.worktree
|
||||||
@@ -684,34 +666,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
running: number
|
running: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefetchChunk = 200
|
const prefetchChunk = 600
|
||||||
const prefetchConcurrency = 1
|
const prefetchConcurrency = 1
|
||||||
const prefetchPendingLimit = 6
|
const prefetchPendingLimit = 6
|
||||||
const prefetchToken = { value: 0 }
|
const prefetchToken = { value: 0 }
|
||||||
const prefetchQueues = new Map<string, PrefetchQueue>()
|
const prefetchQueues = new Map<string, PrefetchQueue>()
|
||||||
|
|
||||||
const PREFETCH_MAX_SESSIONS_PER_DIR = 10
|
|
||||||
const prefetchedByDir = new Map<string, Map<string, true>>()
|
|
||||||
|
|
||||||
const lruFor = (directory: string) => {
|
|
||||||
const existing = prefetchedByDir.get(directory)
|
|
||||||
if (existing) return existing
|
|
||||||
const created = new Map<string, true>()
|
|
||||||
prefetchedByDir.set(directory, created)
|
|
||||||
return created
|
|
||||||
}
|
|
||||||
|
|
||||||
const markPrefetched = (directory: string, sessionID: string) => {
|
|
||||||
const lru = lruFor(directory)
|
|
||||||
if (lru.has(sessionID)) lru.delete(sessionID)
|
|
||||||
lru.set(sessionID, true)
|
|
||||||
while (lru.size > PREFETCH_MAX_SESSIONS_PER_DIR) {
|
|
||||||
const oldest = lru.keys().next().value as string | undefined
|
|
||||||
if (!oldest) return
|
|
||||||
lru.delete(oldest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
params.dir
|
params.dir
|
||||||
globalSDK.url
|
globalSDK.url
|
||||||
@@ -804,11 +764,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (q.inflight.has(session.id)) return
|
if (q.inflight.has(session.id)) return
|
||||||
if (q.pendingSet.has(session.id)) return
|
if (q.pendingSet.has(session.id)) return
|
||||||
|
|
||||||
const lru = lruFor(directory)
|
|
||||||
const known = lru.has(session.id)
|
|
||||||
if (!known && lru.size >= PREFETCH_MAX_SESSIONS_PER_DIR && priority !== "high") return
|
|
||||||
markPrefetched(directory, session.id)
|
|
||||||
|
|
||||||
if (priority === "high") q.pending.unshift(session.id)
|
if (priority === "high") q.pending.unshift(session.id)
|
||||||
if (priority !== "high") q.pending.push(session.id)
|
if (priority !== "high") q.pending.push(session.id)
|
||||||
q.pendingSet.add(session.id)
|
q.pendingSet.add(session.id)
|
||||||
@@ -1136,46 +1091,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (navigate) navigateToProject(directory)
|
if (navigate) navigateToProject(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deepLinkEvent = "opencode:deep-link"
|
|
||||||
|
|
||||||
const parseDeepLink = (input: string) => {
|
|
||||||
if (!input.startsWith("opencode://")) return
|
|
||||||
const url = new URL(input)
|
|
||||||
if (url.hostname !== "open-project") return
|
|
||||||
const directory = url.searchParams.get("directory")
|
|
||||||
if (!directory) return
|
|
||||||
return directory
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeepLinks = (urls: string[]) => {
|
|
||||||
if (!server.isLocal()) return
|
|
||||||
for (const input of urls) {
|
|
||||||
const directory = parseDeepLink(input)
|
|
||||||
if (!directory) continue
|
|
||||||
openProject(directory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const drainDeepLinks = () => {
|
|
||||||
const pending = window.__OPENCODE__?.deepLinks ?? []
|
|
||||||
if (pending.length === 0) return
|
|
||||||
if (window.__OPENCODE__) window.__OPENCODE__.deepLinks = []
|
|
||||||
handleDeepLinks(pending)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const handler = (event: Event) => {
|
|
||||||
const detail = (event as CustomEvent<{ urls: string[] }>).detail
|
|
||||||
const urls = detail?.urls ?? []
|
|
||||||
if (urls.length === 0) return
|
|
||||||
handleDeepLinks(urls)
|
|
||||||
}
|
|
||||||
|
|
||||||
drainDeepLinks()
|
|
||||||
window.addEventListener(deepLinkEvent, handler as EventListener)
|
|
||||||
onCleanup(() => window.removeEventListener(deepLinkEvent, handler as EventListener))
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
|
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
|
||||||
|
|
||||||
async function renameProject(project: LocalProject, next: string) {
|
async function renameProject(project: LocalProject, next: string) {
|
||||||
@@ -1272,7 +1187,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
layout.projects.close(directory)
|
layout.projects.close(directory)
|
||||||
layout.projects.open(root)
|
layout.projects.open(root)
|
||||||
|
|
||||||
if (params.dir && decode64(params.dir) === directory) {
|
if (params.dir && base64Decode(params.dir) === directory) {
|
||||||
navigateToProject(root)
|
navigateToProject(root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1515,8 +1430,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const dir = value.dir
|
const dir = value.dir
|
||||||
const id = value.id
|
const id = value.id
|
||||||
if (!dir || !id) return
|
if (!dir || !id) return
|
||||||
const directory = decode64(dir)
|
const directory = base64Decode(dir)
|
||||||
if (!directory) return
|
|
||||||
setStore("lastSession", directory, id)
|
setStore("lastSession", directory, id)
|
||||||
notification.session.markViewed(id)
|
notification.session.markViewed(id)
|
||||||
const expanded = untrack(() => store.workspaceExpanded[directory])
|
const expanded = untrack(() => store.workspaceExpanded[directory])
|
||||||
@@ -1539,7 +1453,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!project) return
|
if (!project) return
|
||||||
|
|
||||||
if (workspaceSetting()) {
|
if (workspaceSetting()) {
|
||||||
const activeDir = decode64(params.dir) ?? ""
|
const activeDir = params.dir ? base64Decode(params.dir) : ""
|
||||||
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
||||||
for (const directory of dirs) {
|
for (const directory of dirs) {
|
||||||
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
||||||
@@ -1589,7 +1503,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const local = project.worktree
|
const local = project.worktree
|
||||||
const dirs = [local, ...(project.sandboxes ?? [])]
|
const dirs = [local, ...(project.sandboxes ?? [])]
|
||||||
const active = currentProject()
|
const active = currentProject()
|
||||||
const directory = active?.worktree === project.worktree ? decode64(params.dir) : undefined
|
const directory = active?.worktree === project.worktree && params.dir ? base64Decode(params.dir) : undefined
|
||||||
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
|
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
|
||||||
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
||||||
|
|
||||||
@@ -1646,6 +1560,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
|
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
|
||||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||||
|
const mask = "radial-gradient(circle 5px at calc(100% - 4px) 4px, transparent 5px, black 5.5px)"
|
||||||
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1656,7 +1571,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.override}
|
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.override}
|
||||||
{...getAvatarColors(props.project.icon?.color)}
|
{...getAvatarColors(props.project.icon?.color)}
|
||||||
class="size-full rounded"
|
class="size-full rounded"
|
||||||
classList={{ "badge-mask": notifications().length > 0 && props.notify }}
|
style={
|
||||||
|
notifications().length > 0 && props.notify
|
||||||
|
? { "-webkit-mask-image": mask, "mask-image": mask }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Show when={notifications().length > 0 && props.notify}>
|
<Show when={notifications().length > 0 && props.notify}>
|
||||||
@@ -1720,7 +1639,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!user?.agent) return undefined
|
if (!user?.agent) return undefined
|
||||||
|
|
||||||
const agent = sessionStore.agent.find((a) => a.name === user.agent)
|
const agent = sessionStore.agent.find((a) => a.name === user.agent)
|
||||||
return agentColor(user.agent, agent?.color)
|
return agent?.color
|
||||||
})
|
})
|
||||||
|
|
||||||
const hoverMessages = createMemo(() =>
|
const hoverMessages = createMemo(() =>
|
||||||
@@ -1735,22 +1654,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
pendingRename: false,
|
pendingRename: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
|
||||||
const cancelHoverPrefetch = () => {
|
|
||||||
if (hoverPrefetch.current === undefined) return
|
|
||||||
clearTimeout(hoverPrefetch.current)
|
|
||||||
hoverPrefetch.current = undefined
|
|
||||||
}
|
|
||||||
const scheduleHoverPrefetch = () => {
|
|
||||||
if (hoverPrefetch.current !== undefined) return
|
|
||||||
hoverPrefetch.current = setTimeout(() => {
|
|
||||||
hoverPrefetch.current = undefined
|
|
||||||
prefetchSession(props.session)
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
onCleanup(cancelHoverPrefetch)
|
|
||||||
|
|
||||||
const messageLabel = (message: Message) => {
|
const messageLabel = (message: Message) => {
|
||||||
const parts = sessionStore.part[message.id] ?? []
|
const parts = sessionStore.part[message.id] ?? []
|
||||||
const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored)
|
const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored)
|
||||||
@@ -1761,10 +1664,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
<A
|
<A
|
||||||
href={`${props.slug}/session/${props.session.id}`}
|
href={`${props.slug}/session/${props.session.id}`}
|
||||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menu.open ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menu.open ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||||
onPointerEnter={scheduleHoverPrefetch}
|
onMouseEnter={() => prefetchSession(props.session, "high")}
|
||||||
onPointerLeave={cancelHoverPrefetch}
|
|
||||||
onMouseEnter={scheduleHoverPrefetch}
|
|
||||||
onMouseLeave={cancelHoverPrefetch}
|
|
||||||
onFocus={() => prefetchSession(props.session, "high")}
|
onFocus={() => prefetchSession(props.session, "high")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setState("hoverSession", undefined)
|
setState("hoverSession", undefined)
|
||||||
@@ -2029,7 +1929,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
const local = createMemo(() => props.directory === props.project.worktree)
|
const local = createMemo(() => props.directory === props.project.worktree)
|
||||||
const active = createMemo(() => {
|
const active = createMemo(() => {
|
||||||
const current = decode64(params.dir) ?? ""
|
const current = params.dir ? base64Decode(params.dir) : ""
|
||||||
return current === props.directory
|
return current === props.directory
|
||||||
})
|
})
|
||||||
const workspaceValue = createMemo(() => {
|
const workspaceValue = createMemo(() => {
|
||||||
@@ -2039,8 +1939,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local())
|
const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local())
|
||||||
const boot = createMemo(() => open() || active())
|
const boot = createMemo(() => open() || active())
|
||||||
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
|
const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0)
|
||||||
const loading = createMemo(() => open() && !booted() && sessions().length === 0)
|
|
||||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
|
const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
|
||||||
const busy = createMemo(() => isBusy(props.directory))
|
const busy = createMemo(() => isBusy(props.directory))
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
@@ -2231,7 +2130,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
||||||
const sortable = createSortable(props.project.worktree)
|
const sortable = createSortable(props.project.worktree)
|
||||||
const selected = createMemo(() => {
|
const selected = createMemo(() => {
|
||||||
const current = decode64(params.dir) ?? ""
|
const current = params.dir ? base64Decode(params.dir) : ""
|
||||||
return props.project.worktree === current || props.project.sandboxes?.includes(current)
|
return props.project.worktree === current || props.project.sandboxes?.includes(current)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2432,8 +2331,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
})
|
})
|
||||||
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
|
const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0)
|
||||||
const loading = createMemo(() => !booted() && sessions().length === 0)
|
|
||||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
|
const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
setWorkspaceStore("limit", (limit) => limit + 5)
|
setWorkspaceStore("limit", (limit) => limit + 5)
|
||||||
@@ -2445,7 +2343,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
if (!props.mobile) scrollContainerRef = el
|
if (!props.mobile) scrollContainerRef = el
|
||||||
}}
|
}}
|
||||||
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar"
|
||||||
|
style={{ "overflow-anchor": "none" }}
|
||||||
>
|
>
|
||||||
<nav class="flex flex-col gap-1 px-2">
|
<nav class="flex flex-col gap-1 px-2">
|
||||||
<Show when={loading()}>
|
<Show when={loading()}>
|
||||||
@@ -2671,7 +2570,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
if (!panelProps.mobile) scrollContainerRef = el
|
if (!panelProps.mobile) scrollContainerRef = el
|
||||||
}}
|
}}
|
||||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar"
|
||||||
|
style={{ "overflow-anchor": "none" }}
|
||||||
>
|
>
|
||||||
<SortableProvider ids={workspaces()}>
|
<SortableProvider ids={workspaces()}>
|
||||||
<For each={workspaces()}>
|
<For each={workspaces()}>
|
||||||
|
|||||||
+198
-405
@@ -1,16 +1,4 @@
|
|||||||
import {
|
import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
|
||||||
For,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
Show,
|
|
||||||
Match,
|
|
||||||
Switch,
|
|
||||||
createMemo,
|
|
||||||
createEffect,
|
|
||||||
createSignal,
|
|
||||||
on,
|
|
||||||
type JSX,
|
|
||||||
} from "solid-js"
|
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
@@ -23,6 +11,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||||
@@ -39,7 +28,7 @@ import { useSync } from "@/context/sync"
|
|||||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { Terminal } from "@/components/terminal"
|
import { Terminal } from "@/components/terminal"
|
||||||
import { checksum, base64Encode } from "@opencode-ai/util/encode"
|
import { checksum, base64Encode, base64Decode } from "@opencode-ai/util/encode"
|
||||||
import { findLast } from "@opencode-ai/util/array"
|
import { findLast } from "@opencode-ai/util/array"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||||
@@ -58,7 +47,6 @@ import { useComments, type LineComment } from "@/context/comments"
|
|||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { decode64 } from "@/utils/base64"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import {
|
import {
|
||||||
SessionHeader,
|
SessionHeader,
|
||||||
@@ -89,7 +77,6 @@ interface SessionReviewTabProps {
|
|||||||
comments?: LineComment[]
|
comments?: LineComment[]
|
||||||
focusedComment?: { file: string; id: string } | null
|
focusedComment?: { file: string; id: string } | null
|
||||||
onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
|
onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
|
||||||
focusedFile?: string
|
|
||||||
onScrollRef?: (el: HTMLDivElement) => void
|
onScrollRef?: (el: HTMLDivElement) => void
|
||||||
classes?: {
|
classes?: {
|
||||||
root?: string
|
root?: string
|
||||||
@@ -98,44 +85,6 @@ interface SessionReviewTabProps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function StickyAddButton(props: { children: JSX.Element }) {
|
|
||||||
const [stuck, setStuck] = createSignal(false)
|
|
||||||
let button: HTMLDivElement | undefined
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const node = button
|
|
||||||
if (!node) return
|
|
||||||
|
|
||||||
const scroll = node.parentElement
|
|
||||||
if (!scroll) return
|
|
||||||
|
|
||||||
const handler = () => {
|
|
||||||
const rect = node.getBoundingClientRect()
|
|
||||||
const scrollRect = scroll.getBoundingClientRect()
|
|
||||||
setStuck(rect.right >= scrollRect.right && scroll.scrollWidth > scroll.clientWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
scroll.addEventListener("scroll", handler, { passive: true })
|
|
||||||
const observer = new ResizeObserver(handler)
|
|
||||||
observer.observe(scroll)
|
|
||||||
handler()
|
|
||||||
onCleanup(() => {
|
|
||||||
scroll.removeEventListener("scroll", handler)
|
|
||||||
observer.disconnect()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={button}
|
|
||||||
class="bg-background-base h-full shrink-0 sticky right-0 z-10 flex items-center justify-center border-b border-border-weak-base px-3"
|
|
||||||
classList={{ "border-l": stuck() }}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SessionReviewTab(props: SessionReviewTabProps) {
|
function SessionReviewTab(props: SessionReviewTabProps) {
|
||||||
let scroll: HTMLDivElement | undefined
|
let scroll: HTMLDivElement | undefined
|
||||||
let frame: number | undefined
|
let frame: number | undefined
|
||||||
@@ -214,7 +163,6 @@ function SessionReviewTab(props: SessionReviewTabProps) {
|
|||||||
diffStyle={props.diffStyle}
|
diffStyle={props.diffStyle}
|
||||||
onDiffStyleChange={props.onDiffStyleChange}
|
onDiffStyleChange={props.onDiffStyleChange}
|
||||||
onViewFile={props.onViewFile}
|
onViewFile={props.onViewFile}
|
||||||
focusedFile={props.focusedFile}
|
|
||||||
readFile={readFile}
|
readFile={readFile}
|
||||||
onLineComment={props.onLineComment}
|
onLineComment={props.onLineComment}
|
||||||
comments={props.comments}
|
comments={props.comments}
|
||||||
@@ -324,7 +272,6 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const centered = createMemo(() => isDesktop() && !layout.fileTree.opened())
|
|
||||||
|
|
||||||
function normalizeTab(tab: string) {
|
function normalizeTab(tab: string) {
|
||||||
if (!tab.startsWith("file://")) return tab
|
if (!tab.startsWith("file://")) return tab
|
||||||
@@ -378,8 +325,7 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
const reviewCount = createMemo(() => info()?.summary?.files ?? 0)
|
||||||
const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
|
||||||
const hasReview = createMemo(() => reviewCount() > 0)
|
const hasReview = createMemo(() => reviewCount() > 0)
|
||||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||||
@@ -435,7 +381,7 @@ export default function Page() {
|
|||||||
expanded: {} as Record<string, boolean>,
|
expanded: {} as Record<string, boolean>,
|
||||||
messageId: undefined as string | undefined,
|
messageId: undefined as string | undefined,
|
||||||
turnStart: 0,
|
turnStart: 0,
|
||||||
mobileTab: "session" as "session" | "changes",
|
mobileTab: "session" as "session" | "review",
|
||||||
newSessionWorktree: "main",
|
newSessionWorktree: "main",
|
||||||
promptHeight: 0,
|
promptHeight: 0,
|
||||||
})
|
})
|
||||||
@@ -479,42 +425,10 @@ export default function Page() {
|
|||||||
const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset
|
const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset
|
||||||
if (targetIndex < 0 || targetIndex >= msgs.length) return
|
if (targetIndex < 0 || targetIndex >= msgs.length) return
|
||||||
|
|
||||||
if (targetIndex === msgs.length - 1) {
|
|
||||||
resumeScroll()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
autoScroll.pause()
|
|
||||||
scrollToMessage(msgs[targetIndex], "auto")
|
scrollToMessage(msgs[targetIndex], "auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
const kinds = createMemo(() => {
|
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||||
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
|
||||||
if (!a) return b
|
|
||||||
if (a === b) return a
|
|
||||||
return "mix" as const
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
|
||||||
|
|
||||||
const out = new Map<string, "add" | "del" | "mix">()
|
|
||||||
for (const diff of diffs()) {
|
|
||||||
const file = normalize(diff.file)
|
|
||||||
const add = diff.additions > 0
|
|
||||||
const del = diff.deletions > 0
|
|
||||||
const kind = add && del ? "mix" : add ? "add" : del ? "del" : "mix"
|
|
||||||
|
|
||||||
out.set(file, kind)
|
|
||||||
|
|
||||||
const parts = file.split("/")
|
|
||||||
for (const [idx] of parts.slice(0, -1).entries()) {
|
|
||||||
const dir = parts.slice(0, idx + 1).join("/")
|
|
||||||
if (!dir) continue
|
|
||||||
out.set(dir, merge(out.get(dir), kind))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
const emptyDiffFiles: string[] = []
|
const emptyDiffFiles: string[] = []
|
||||||
const diffFiles = createMemo(() => diffs().map((d) => d.file), emptyDiffFiles, { equals: same })
|
const diffFiles = createMemo(() => diffs().map((d) => d.file), emptyDiffFiles, { equals: same })
|
||||||
const diffsReady = createMemo(() => {
|
const diffsReady = createMemo(() => {
|
||||||
@@ -531,8 +445,6 @@ export default function Page() {
|
|||||||
|
|
||||||
const scrollGestureWindowMs = 250
|
const scrollGestureWindowMs = 250
|
||||||
|
|
||||||
let touchGesture: number | undefined
|
|
||||||
|
|
||||||
const markScrollGesture = (target?: EventTarget | null) => {
|
const markScrollGesture = (target?: EventTarget | null) => {
|
||||||
const root = scroller
|
const root = scroller
|
||||||
if (!root) return
|
if (!root) return
|
||||||
@@ -687,7 +599,7 @@ export default function Page() {
|
|||||||
category: language.t("command.category.file"),
|
category: language.t("command.category.file"),
|
||||||
keybind: "mod+p",
|
keybind: "mod+p",
|
||||||
slash: "open",
|
slash: "open",
|
||||||
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={() => showAllFiles()} />),
|
onSelect: () => dialog.show(() => <DialogSelectFile />),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "context.addSelection",
|
id: "context.addSelection",
|
||||||
@@ -735,7 +647,7 @@ export default function Page() {
|
|||||||
description: "",
|
description: "",
|
||||||
category: language.t("command.category.view"),
|
category: language.t("command.category.view"),
|
||||||
keybind: "mod+shift+r",
|
keybind: "mod+shift+r",
|
||||||
onSelect: () => layout.fileTree.toggle(),
|
onSelect: () => view().reviewPanel.toggle(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "terminal.new",
|
id: "terminal.new",
|
||||||
@@ -1104,81 +1016,27 @@ export default function Page() {
|
|||||||
.filter((tab) => tab !== "context"),
|
.filter((tab) => tab !== "context"),
|
||||||
)
|
)
|
||||||
|
|
||||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
const mobileReview = createMemo(() => !isDesktop() && view().reviewPanel.opened() && store.mobileTab === "review")
|
||||||
|
|
||||||
const fileTreeTab = () => layout.fileTree.tab()
|
const showTabs = createMemo(() => view().reviewPanel.opened())
|
||||||
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
|
|
||||||
|
|
||||||
const [tree, setTree] = createStore({
|
const [tree, setTree] = createStore({
|
||||||
|
fileTreeTab: "changes" as "changes" | "all",
|
||||||
reviewScroll: undefined as HTMLDivElement | undefined,
|
reviewScroll: undefined as HTMLDivElement | undefined,
|
||||||
pendingDiff: undefined as string | undefined,
|
pendingDiff: undefined as string | undefined,
|
||||||
activeDiff: undefined as string | undefined,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fileTreeTab = () => tree.fileTreeTab
|
||||||
|
const setFileTreeTab = (value: "changes" | "all") => setTree("fileTreeTab", value)
|
||||||
const reviewScroll = () => tree.reviewScroll
|
const reviewScroll = () => tree.reviewScroll
|
||||||
const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value)
|
const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value)
|
||||||
const pendingDiff = () => tree.pendingDiff
|
const pendingDiff = () => tree.pendingDiff
|
||||||
const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value)
|
const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value)
|
||||||
const activeDiff = () => tree.activeDiff
|
|
||||||
const setActiveDiff = (value: string | undefined) => setTree("activeDiff", value)
|
|
||||||
|
|
||||||
const showAllFiles = () => {
|
createEffect(() => {
|
||||||
if (fileTreeTab() !== "changes") return
|
if (!layout.fileTree.opened()) return
|
||||||
setFileTreeTab("all")
|
setFileTreeTab("changes")
|
||||||
}
|
})
|
||||||
|
|
||||||
const reviewPanel = () => (
|
|
||||||
<div class="flex flex-col h-full overflow-hidden bg-background-stronger contain-strict">
|
|
||||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
|
||||||
<Switch>
|
|
||||||
<Match when={hasReview()}>
|
|
||||||
<Show
|
|
||||||
when={diffsReady()}
|
|
||||||
fallback={<div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div>}
|
|
||||||
>
|
|
||||||
<SessionReviewTab
|
|
||||||
diffs={diffs}
|
|
||||||
view={view}
|
|
||||||
diffStyle={layout.review.diffStyle()}
|
|
||||||
onDiffStyleChange={layout.review.setDiffStyle}
|
|
||||||
onScrollRef={setReviewScroll}
|
|
||||||
focusedFile={activeDiff()}
|
|
||||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
|
||||||
comments={comments.all()}
|
|
||||||
focusedComment={comments.focus()}
|
|
||||||
onFocusedCommentChange={comments.setFocus}
|
|
||||||
onViewFile={(path) => {
|
|
||||||
showAllFiles()
|
|
||||||
const value = file.tab(path)
|
|
||||||
tabs().open(value)
|
|
||||||
file.load(path)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6">
|
|
||||||
<Mark class="w-14 opacity-10" />
|
|
||||||
<div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => tabs().active(),
|
|
||||||
(active) => {
|
|
||||||
if (!active) return
|
|
||||||
if (fileTreeTab() !== "changes") return
|
|
||||||
if (!file.pathFromTab(active)) return
|
|
||||||
showAllFiles()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const setFileTreeTabValue = (value: string) => {
|
const setFileTreeTabValue = (value: string) => {
|
||||||
if (value !== "changes" && value !== "all") return
|
if (value !== "changes" && value !== "all") return
|
||||||
@@ -1222,7 +1080,6 @@ export default function Page() {
|
|||||||
const focusReviewDiff = (path: string) => {
|
const focusReviewDiff = (path: string) => {
|
||||||
const current = view().review.open() ?? []
|
const current = view().review.open() ?? []
|
||||||
if (!current.includes(path)) view().review.setOpen([...current, path])
|
if (!current.includes(path)) view().review.setOpen([...current, path])
|
||||||
setActiveDiff(path)
|
|
||||||
setPendingDiff(path)
|
setPendingDiff(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1269,44 +1126,54 @@ export default function Page() {
|
|||||||
|
|
||||||
const activeTab = createMemo(() => {
|
const activeTab = createMemo(() => {
|
||||||
const active = tabs().active()
|
const active = tabs().active()
|
||||||
if (active === "context") return "context"
|
if (layout.fileTree.opened() && fileTreeTab() === "all") {
|
||||||
if (active && file.pathFromTab(active)) return normalizeTab(active)
|
if (active && active !== "review" && active !== "context") return normalizeTab(active)
|
||||||
|
|
||||||
|
const first = openedTabs()[0]
|
||||||
|
if (first) return first
|
||||||
|
return "review"
|
||||||
|
}
|
||||||
|
if (active) return normalizeTab(active)
|
||||||
|
if (hasReview()) return "review"
|
||||||
|
|
||||||
const first = openedTabs()[0]
|
const first = openedTabs()[0]
|
||||||
if (first) return first
|
if (first) return first
|
||||||
if (contextOpen()) return "context"
|
if (contextOpen()) return "context"
|
||||||
return "empty"
|
return "review"
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!layout.ready()) return
|
if (!layout.ready()) return
|
||||||
if (tabs().active()) return
|
if (tabs().active()) return
|
||||||
if (openedTabs().length === 0 && !contextOpen()) return
|
if (!hasReview() && openedTabs().length === 0 && !contextOpen()) return
|
||||||
|
tabs().setActive(activeTab())
|
||||||
|
})
|
||||||
|
|
||||||
const next = activeTab()
|
createEffect(() => {
|
||||||
if (next === "empty") return
|
if (!layout.fileTree.opened()) return
|
||||||
tabs().setActive(next)
|
if (fileTreeTab() !== "all") return
|
||||||
|
|
||||||
|
const first = openedTabs()[0]
|
||||||
|
if (!first) return
|
||||||
|
|
||||||
|
const active = tabs().active()
|
||||||
|
if (active && active !== "review" && active !== "context") return
|
||||||
|
tabs().setActive(first)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
if (!hasReview()) return
|
||||||
|
|
||||||
const wants = isDesktop() ? layout.fileTree.opened() && fileTreeTab() === "changes" : store.mobileTab === "changes"
|
const wants = isDesktop()
|
||||||
|
? view().reviewPanel.opened() &&
|
||||||
|
(layout.fileTree.opened() ? fileTreeTab() === "changes" : activeTab() === "review")
|
||||||
|
: store.mobileTab === "review"
|
||||||
if (!wants) return
|
if (!wants) return
|
||||||
if (sync.data.session_diff[id] !== undefined) return
|
if (diffsReady()) return
|
||||||
if (sync.status === "loading") return
|
|
||||||
|
|
||||||
void sync.session.diff(id)
|
sync.session.diff(id)
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!isDesktop()) return
|
|
||||||
if (!layout.fileTree.opened()) return
|
|
||||||
if (sync.status === "loading") return
|
|
||||||
|
|
||||||
fileTreeTab()
|
|
||||||
void file.tree.list("")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const autoScroll = createAutoScroll({
|
const autoScroll = createAutoScroll({
|
||||||
@@ -1314,15 +1181,9 @@ export default function Page() {
|
|||||||
overflowAnchor: "dynamic",
|
overflowAnchor: "dynamic",
|
||||||
})
|
})
|
||||||
|
|
||||||
const clearMessageHash = () => {
|
|
||||||
if (!window.location.hash) return
|
|
||||||
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
const resumeScroll = () => {
|
const resumeScroll = () => {
|
||||||
setStore("messageId", undefined)
|
setStore("messageId", undefined)
|
||||||
autoScroll.forceScrollToBottom()
|
autoScroll.forceScrollToBottom()
|
||||||
clearMessageHash()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the user returns to the bottom, treat the active message as "latest".
|
// When the user returns to the bottom, treat the active message as "latest".
|
||||||
@@ -1332,7 +1193,6 @@ export default function Page() {
|
|||||||
(scrolled) => {
|
(scrolled) => {
|
||||||
if (scrolled) return
|
if (scrolled) return
|
||||||
setStore("messageId", undefined)
|
setStore("messageId", undefined)
|
||||||
clearMessageHash()
|
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
@@ -1407,8 +1267,7 @@ export default function Page() {
|
|||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const delta = el.scrollHeight - beforeHeight
|
const delta = el.scrollHeight - beforeHeight
|
||||||
if (!delta) return
|
if (delta) el.scrollTop = beforeTop + delta
|
||||||
el.scrollTop = beforeTop + delta
|
|
||||||
})
|
})
|
||||||
|
|
||||||
scheduleTurnBackfill()
|
scheduleTurnBackfill()
|
||||||
@@ -1539,7 +1398,6 @@ export default function Page() {
|
|||||||
|
|
||||||
const match = hash.match(/^message-(.+)$/)
|
const match = hash.match(/^message-(.+)$/)
|
||||||
if (match) {
|
if (match) {
|
||||||
autoScroll.pause()
|
|
||||||
const msg = visibleUserMessages().find((m) => m.id === match[1])
|
const msg = visibleUserMessages().find((m) => m.id === match[1])
|
||||||
if (msg) {
|
if (msg) {
|
||||||
scrollToMessage(msg, behavior)
|
scrollToMessage(msg, behavior)
|
||||||
@@ -1553,7 +1411,6 @@ export default function Page() {
|
|||||||
|
|
||||||
const target = document.getElementById(hash)
|
const target = document.getElementById(hash)
|
||||||
if (target) {
|
if (target) {
|
||||||
autoScroll.pause()
|
|
||||||
scrollToElement(target, behavior)
|
scrollToElement(target, behavior)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1650,7 +1507,6 @@ export default function Page() {
|
|||||||
const msg = visibleUserMessages().find((m) => m.id === targetId)
|
const msg = visibleUserMessages().find((m) => m.id === targetId)
|
||||||
if (!msg) return
|
if (!msg) return
|
||||||
if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined)
|
if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined)
|
||||||
autoScroll.pause()
|
|
||||||
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
|
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1728,8 +1584,8 @@ export default function Page() {
|
|||||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||||
<SessionHeader />
|
<SessionHeader />
|
||||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||||
{/* Mobile tab bar */}
|
{/* Mobile tab bar - only shown on mobile when user opened review */}
|
||||||
<Show when={!isDesktop() && params.id}>
|
<Show when={!isDesktop() && view().reviewPanel.opened()}>
|
||||||
<Tabs class="h-auto">
|
<Tabs class="h-auto">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
@@ -1741,16 +1597,16 @@ export default function Page() {
|
|||||||
{language.t("session.tab.session")}
|
{language.t("session.tab.session")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="changes"
|
value="review"
|
||||||
class="w-1/2 !border-r-0"
|
class="w-1/2 !border-r-0"
|
||||||
classes={{ button: "w-full" }}
|
classes={{ button: "w-full" }}
|
||||||
onClick={() => setStore("mobileTab", "changes")}
|
onClick={() => setStore("mobileTab", "review")}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={hasReview()}>
|
<Match when={hasReview()}>
|
||||||
{language.t("session.review.filesChanged", { count: reviewCount() })}
|
{language.t("session.review.filesChanged", { count: reviewCount() })}
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>{language.t("session.review.change.other")}</Match>
|
<Match when={true}>{language.t("session.tab.review")}</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
@@ -1761,11 +1617,10 @@ export default function Page() {
|
|||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true,
|
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true,
|
||||||
"flex-1 pt-6 md:pt-3": true,
|
"flex-1 md:flex-none pt-6 md:pt-3": true,
|
||||||
"md:flex-none": layout.fileTree.opened(),
|
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: isDesktop() && layout.fileTree.opened() ? `${layout.session.width()}px` : "100%",
|
width: isDesktop() && showTabs() ? `${layout.session.width()}px` : "100%",
|
||||||
"--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined,
|
"--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1774,7 +1629,7 @@ export default function Page() {
|
|||||||
<Match when={params.id}>
|
<Match when={params.id}>
|
||||||
<Show when={activeMessage()}>
|
<Show when={activeMessage()}>
|
||||||
<Show
|
<Show
|
||||||
when={!mobileChanges()}
|
when={!mobileReview()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="relative h-full overflow-hidden">
|
<div class="relative h-full overflow-hidden">
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -1791,13 +1646,11 @@ export default function Page() {
|
|||||||
diffs={diffs}
|
diffs={diffs}
|
||||||
view={view}
|
view={view}
|
||||||
diffStyle="unified"
|
diffStyle="unified"
|
||||||
focusedFile={activeDiff()}
|
|
||||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||||
comments={comments.all()}
|
comments={comments.all()}
|
||||||
focusedComment={comments.focus()}
|
focusedComment={comments.focus()}
|
||||||
onFocusedCommentChange={comments.setFocus}
|
onFocusedCommentChange={comments.setFocus}
|
||||||
onViewFile={(path) => {
|
onViewFile={(path) => {
|
||||||
showAllFiles()
|
|
||||||
const value = file.tab(path)
|
const value = file.tab(path)
|
||||||
tabs().open(value)
|
tabs().open(value)
|
||||||
file.load(path)
|
file.load(path)
|
||||||
@@ -1813,7 +1666,7 @@ export default function Page() {
|
|||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<div class="h-full px-4 pb-30 flex flex-col items-center justify-center text-center gap-6">
|
<div class="h-full px-4 pb-30 flex flex-col items-center justify-center text-center gap-6">
|
||||||
<Mark class="w-14 opacity-10" />
|
<Mark class="w-14 opacity-10" />
|
||||||
<div class="text-14-regular text-text-weak max-w-56">
|
<div class="text-13-regular text-text-weak max-w-56">
|
||||||
{language.t("session.review.empty")}
|
{language.t("session.review.empty")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1832,102 +1685,27 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="pointer-events-auto size-8 flex items-center justify-center rounded-full bg-background-base border border-border-base shadow-sm text-text-base hover:bg-background-stronger transition-colors"
|
class="pointer-events-auto size-8 flex items-center justify-center rounded-full bg-background-base border border-border-base shadow-sm text-text-base hover:bg-background-stronger transition-colors"
|
||||||
onClick={resumeScroll}
|
onClick={() => {
|
||||||
|
setStore("messageId", undefined)
|
||||||
|
autoScroll.forceScrollToBottom()
|
||||||
|
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="arrow-down-to-line" />
|
<Icon name="arrow-down-to-line" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={setScrollRef}
|
ref={setScrollRef}
|
||||||
onWheel={(e) => {
|
onWheel={(e) => markScrollGesture(e.target)}
|
||||||
const root = e.currentTarget
|
onTouchMove={(e) => markScrollGesture(e.target)}
|
||||||
const target = e.target instanceof Element ? e.target : undefined
|
|
||||||
const nested = target?.closest("[data-scrollable]")
|
|
||||||
if (!nested || nested === root) {
|
|
||||||
markScrollGesture(root)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(nested instanceof HTMLElement)) {
|
|
||||||
markScrollGesture(root)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const max = nested.scrollHeight - nested.clientHeight
|
|
||||||
if (max <= 1) {
|
|
||||||
markScrollGesture(root)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const delta =
|
|
||||||
e.deltaMode === 1
|
|
||||||
? e.deltaY * 40
|
|
||||||
: e.deltaMode === 2
|
|
||||||
? e.deltaY * root.clientHeight
|
|
||||||
: e.deltaY
|
|
||||||
if (!delta) return
|
|
||||||
|
|
||||||
if (delta < 0) {
|
|
||||||
if (nested.scrollTop + delta <= 0) markScrollGesture(root)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const remaining = max - nested.scrollTop
|
|
||||||
if (delta > remaining) markScrollGesture(root)
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
touchGesture = e.touches[0]?.clientY
|
|
||||||
}}
|
|
||||||
onTouchMove={(e) => {
|
|
||||||
const next = e.touches[0]?.clientY
|
|
||||||
const prev = touchGesture
|
|
||||||
touchGesture = next
|
|
||||||
if (next === undefined || prev === undefined) return
|
|
||||||
|
|
||||||
const delta = prev - next
|
|
||||||
if (!delta) return
|
|
||||||
|
|
||||||
const root = e.currentTarget
|
|
||||||
const target = e.target instanceof Element ? e.target : undefined
|
|
||||||
const nested = target?.closest("[data-scrollable]")
|
|
||||||
if (!nested || nested === root) {
|
|
||||||
markScrollGesture(root)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(nested instanceof HTMLElement)) {
|
|
||||||
markScrollGesture(root)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const max = nested.scrollHeight - nested.clientHeight
|
|
||||||
if (max <= 1) {
|
|
||||||
markScrollGesture(root)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delta < 0) {
|
|
||||||
if (nested.scrollTop + delta <= 0) markScrollGesture(root)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const remaining = max - nested.scrollTop
|
|
||||||
if (delta > remaining) markScrollGesture(root)
|
|
||||||
}}
|
|
||||||
onTouchEnd={() => {
|
|
||||||
touchGesture = undefined
|
|
||||||
}}
|
|
||||||
onTouchCancel={() => {
|
|
||||||
touchGesture = undefined
|
|
||||||
}}
|
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
if (e.target !== e.currentTarget) return
|
if (e.target !== e.currentTarget) return
|
||||||
markScrollGesture(e.currentTarget)
|
markScrollGesture(e.target)
|
||||||
}}
|
}}
|
||||||
onScroll={(e) => {
|
onScroll={(e) => {
|
||||||
if (!hasScrollGesture()) return
|
if (!hasScrollGesture()) return
|
||||||
|
markScrollGesture(e.target)
|
||||||
autoScroll.handleScroll()
|
autoScroll.handleScroll()
|
||||||
markScrollGesture(e.currentTarget)
|
|
||||||
if (isDesktop()) scheduleScrollSpy(e.currentTarget)
|
if (isDesktop()) scheduleScrollSpy(e.currentTarget)
|
||||||
}}
|
}}
|
||||||
onClick={autoScroll.handleInteraction}
|
onClick={autoScroll.handleInteraction}
|
||||||
@@ -1940,7 +1718,7 @@ export default function Page() {
|
|||||||
"sticky top-0 z-30 bg-background-stronger": true,
|
"sticky top-0 z-30 bg-background-stronger": true,
|
||||||
"w-full": true,
|
"w-full": true,
|
||||||
"px-4 md:px-6": true,
|
"px-4 md:px-6": true,
|
||||||
"md:max-w-200 md:mx-auto": centered(),
|
"md:max-w-200 md:mx-auto": !showTabs(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="h-10 flex items-center gap-1">
|
<div class="h-10 flex items-center gap-1">
|
||||||
@@ -1968,9 +1746,9 @@ export default function Page() {
|
|||||||
class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
||||||
classList={{
|
classList={{
|
||||||
"w-full": true,
|
"w-full": true,
|
||||||
"md:max-w-200 md:mx-auto": centered(),
|
"md:max-w-200 md:mx-auto": !showTabs(),
|
||||||
"mt-0.5": centered(),
|
"mt-0.5": !showTabs(),
|
||||||
"mt-0": !centered(),
|
"mt-0": showTabs(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={store.turnStart > 0}>
|
<Show when={store.turnStart > 0}>
|
||||||
@@ -2021,7 +1799,7 @@ export default function Page() {
|
|||||||
data-message-id={message.id}
|
data-message-id={message.id}
|
||||||
classList={{
|
classList={{
|
||||||
"min-w-0 w-full max-w-full": true,
|
"min-w-0 w-full max-w-full": true,
|
||||||
"md:max-w-200": centered(),
|
"md:max-w-200": !showTabs(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SessionTurn
|
<SessionTurn
|
||||||
@@ -2078,7 +1856,7 @@ export default function Page() {
|
|||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"w-full px-4 pointer-events-auto": true,
|
"w-full px-4 pointer-events-auto": true,
|
||||||
"md:max-w-200 md:mx-auto": centered(),
|
"md:max-w-200": !showTabs(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={request()} keyed>
|
<Show when={request()} keyed>
|
||||||
@@ -2151,7 +1929,7 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={isDesktop() && layout.fileTree.opened()}>
|
<Show when={isDesktop() && showTabs()}>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
size={layout.session.width()}
|
size={layout.session.width()}
|
||||||
@@ -2162,8 +1940,8 @@ export default function Page() {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop side panel - hidden on mobile */}
|
{/* Desktop tabs panel (Review + Context + Files) - hidden on mobile */}
|
||||||
<Show when={isDesktop() && layout.fileTree.opened()}>
|
<Show when={isDesktop() && showTabs()}>
|
||||||
<aside
|
<aside
|
||||||
id="review-panel"
|
id="review-panel"
|
||||||
aria-label={language.t("session.panel.reviewAndFiles")}
|
aria-label={language.t("session.panel.reviewAndFiles")}
|
||||||
@@ -2171,7 +1949,7 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
<div class="flex-1 min-w-0 h-full">
|
<div class="flex-1 min-w-0 h-full">
|
||||||
<Show
|
<Show
|
||||||
when={fileTreeTab() === "changes"}
|
when={layout.fileTree.opened() && fileTreeTab() === "changes"}
|
||||||
fallback={
|
fallback={
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
@@ -2183,71 +1961,32 @@ export default function Page() {
|
|||||||
<ConstrainDragYAxis />
|
<ConstrainDragYAxis />
|
||||||
<Tabs value={activeTab()} onChange={openTab}>
|
<Tabs value={activeTab()} onChange={openTab}>
|
||||||
<div class="sticky top-0 shrink-0 flex">
|
<div class="sticky top-0 shrink-0 flex">
|
||||||
<Tabs.List
|
<Tabs.List>
|
||||||
ref={(el: HTMLDivElement) => {
|
<Show when={!layout.fileTree.opened()}>
|
||||||
let scrollTimeout: number | undefined
|
<Tabs.Trigger value="review">
|
||||||
let prevScrollWidth = el.scrollWidth
|
<div class="flex items-center gap-3">
|
||||||
let prevContextOpen = contextOpen()
|
<Show when={diffs()}>
|
||||||
|
<DiffChanges changes={diffs()} variant="bars" />
|
||||||
const handler = () => {
|
</Show>
|
||||||
if (scrollTimeout !== undefined) clearTimeout(scrollTimeout)
|
<div class="flex items-center gap-1.5">
|
||||||
scrollTimeout = window.setTimeout(() => {
|
<div>{language.t("session.tab.review")}</div>
|
||||||
const scrollWidth = el.scrollWidth
|
<Show when={info()?.summary?.files}>
|
||||||
const clientWidth = el.clientWidth
|
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
|
||||||
const currentContextOpen = contextOpen()
|
{info()?.summary?.files ?? 0}
|
||||||
|
</div>
|
||||||
// Only scroll when a tab is added (width increased), not on removal
|
</Show>
|
||||||
if (scrollWidth > prevScrollWidth) {
|
</div>
|
||||||
if (!prevContextOpen && currentContextOpen) {
|
</div>
|
||||||
// Context tab was opened, scroll to first
|
</Tabs.Trigger>
|
||||||
el.scrollTo({
|
</Show>
|
||||||
left: 0,
|
<Show when={!layout.fileTree.opened() && contextOpen()}>
|
||||||
behavior: "smooth",
|
|
||||||
})
|
|
||||||
} else if (scrollWidth > clientWidth) {
|
|
||||||
// File tab was added, scroll to rightmost
|
|
||||||
el.scrollTo({
|
|
||||||
left: scrollWidth - clientWidth,
|
|
||||||
behavior: "smooth",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// When width decreases (tab removed), don't scroll - let browser handle it naturally
|
|
||||||
|
|
||||||
prevScrollWidth = scrollWidth
|
|
||||||
prevContextOpen = currentContextOpen
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const wheelHandler = (e: WheelEvent) => {
|
|
||||||
// Enable horizontal scrolling with mouse wheel
|
|
||||||
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
||||||
el.scrollLeft += e.deltaY > 0 ? 50 : -50
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
el.addEventListener("wheel", wheelHandler, { passive: false })
|
|
||||||
|
|
||||||
const observer = new MutationObserver(handler)
|
|
||||||
observer.observe(el, { childList: true })
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
el.removeEventListener("wheel", wheelHandler)
|
|
||||||
observer.disconnect()
|
|
||||||
if (scrollTimeout !== undefined) clearTimeout(scrollTimeout)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={contextOpen()}>
|
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="context"
|
value="context"
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="close-small"
|
icon="close"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-5 w-5"
|
|
||||||
onClick={() => tabs().close("context")}
|
onClick={() => tabs().close("context")}
|
||||||
aria-label={language.t("common.closeTab")}
|
aria-label={language.t("common.closeTab")}
|
||||||
/>
|
/>
|
||||||
@@ -2267,7 +2006,7 @@ export default function Page() {
|
|||||||
{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}
|
{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}
|
||||||
</For>
|
</For>
|
||||||
</SortableProvider>
|
</SortableProvider>
|
||||||
<StickyAddButton>
|
<div class="bg-background-base h-full shrink-0 sticky right-0 z-10 flex items-center justify-center border-b border-l border-border-weak-base px-3">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
title={language.t("command.file.open")}
|
title={language.t("command.file.open")}
|
||||||
keybind={command.keybind("file.open")}
|
keybind={command.keybind("file.open")}
|
||||||
@@ -2277,30 +2016,71 @@ export default function Page() {
|
|||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
iconSize="large"
|
iconSize="large"
|
||||||
onClick={() =>
|
onClick={() => dialog.show(() => <DialogSelectFile mode="files" />)}
|
||||||
dialog.show(() => <DialogSelectFile mode="files" onOpenFile={() => showAllFiles()} />)
|
|
||||||
}
|
|
||||||
aria-label={language.t("command.file.open")}
|
aria-label={language.t("command.file.open")}
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</StickyAddButton>
|
</div>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={!layout.fileTree.opened()}>
|
||||||
|
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
|
<Show when={activeTab() === "review"}>
|
||||||
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
|
<Switch>
|
||||||
|
<Match when={hasReview()}>
|
||||||
|
<Show
|
||||||
|
when={diffsReady()}
|
||||||
|
fallback={
|
||||||
|
<div class="px-6 py-4 text-text-weak">
|
||||||
|
{language.t("session.review.loadingChanges")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SessionReviewTab
|
||||||
|
diffs={diffs}
|
||||||
|
view={view}
|
||||||
|
diffStyle={layout.review.diffStyle()}
|
||||||
|
onDiffStyleChange={layout.review.setDiffStyle}
|
||||||
|
onScrollRef={setReviewScroll}
|
||||||
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||||
|
comments={comments.all()}
|
||||||
|
focusedComment={comments.focus()}
|
||||||
|
onFocusedCommentChange={comments.setFocus}
|
||||||
|
onViewFile={(path) => {
|
||||||
|
const value = file.tab(path)
|
||||||
|
tabs().open(value)
|
||||||
|
file.load(path)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6">
|
||||||
|
<Mark class="w-14 opacity-10" />
|
||||||
|
<div class="text-13-regular text-text-weak max-w-56">
|
||||||
|
{language.t("session.review.empty")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
<Show when={layout.fileTree.opened() && fileTreeTab() === "all" && openedTabs().length === 0}>
|
||||||
<Show when={activeTab() === "empty"}>
|
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
<div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6">
|
||||||
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
|
<Mark class="w-14 opacity-10" />
|
||||||
<Mark class="w-14 opacity-10" />
|
<div class="text-13-regular text-text-weak max-w-56">
|
||||||
<div class="text-14-regular text-text-weak max-w-56">
|
{language.t("session.files.selectToOpen")}
|
||||||
{language.t("session.files.selectToOpen")}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Tabs.Content>
|
||||||
</Tabs.Content>
|
</Show>
|
||||||
|
|
||||||
<Show when={contextOpen()}>
|
<Show when={!layout.fileTree.opened() && contextOpen()}>
|
||||||
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
<Show when={activeTab() === "context"}>
|
<Show when={activeTab() === "context"}>
|
||||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
@@ -2346,28 +2126,8 @@ export default function Page() {
|
|||||||
if (!isSvg()) return
|
if (!isSvg()) return
|
||||||
const c = state()?.content
|
const c = state()?.content
|
||||||
if (!c) return
|
if (!c) return
|
||||||
if (c.encoding !== "base64") return c.content
|
if (c.encoding === "base64") return base64Decode(c.content)
|
||||||
return decode64(c.content)
|
return c.content
|
||||||
})
|
|
||||||
|
|
||||||
const svgDecodeFailed = createMemo(() => {
|
|
||||||
if (!isSvg()) return false
|
|
||||||
const c = state()?.content
|
|
||||||
if (!c) return false
|
|
||||||
if (c.encoding !== "base64") return false
|
|
||||||
return svgContent() === undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const svgToast = { shown: false }
|
|
||||||
createEffect(() => {
|
|
||||||
if (!svgDecodeFailed()) return
|
|
||||||
if (svgToast.shown) return
|
|
||||||
svgToast.shown = true
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("toast.file.loadFailed.title"),
|
|
||||||
description: "Invalid base64 content.",
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
const svgPreviewUrl = createMemo(() => {
|
const svgPreviewUrl = createMemo(() => {
|
||||||
if (!isSvg()) return
|
if (!isSvg()) return
|
||||||
@@ -2822,16 +2582,50 @@ export default function Page() {
|
|||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{reviewPanel()}
|
<div class="flex flex-col h-full overflow-hidden bg-background-stronger contain-strict">
|
||||||
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
|
<Switch>
|
||||||
|
<Match when={hasReview()}>
|
||||||
|
<Show
|
||||||
|
when={diffsReady()}
|
||||||
|
fallback={
|
||||||
|
<div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SessionReviewTab
|
||||||
|
diffs={diffs}
|
||||||
|
view={view}
|
||||||
|
diffStyle={layout.review.diffStyle()}
|
||||||
|
onDiffStyleChange={layout.review.setDiffStyle}
|
||||||
|
onScrollRef={setReviewScroll}
|
||||||
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||||
|
comments={comments.all()}
|
||||||
|
focusedComment={comments.focus()}
|
||||||
|
onFocusedCommentChange={comments.setFocus}
|
||||||
|
onViewFile={(path) => {
|
||||||
|
const value = file.tab(path)
|
||||||
|
tabs().open(value)
|
||||||
|
file.load(path)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6">
|
||||||
|
<Mark class="w-14 opacity-10" />
|
||||||
|
<div class="text-13-regular text-text-weak max-w-56">
|
||||||
|
{language.t("session.review.empty")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={layout.fileTree.opened()}>
|
<Show when={layout.fileTree.opened()}>
|
||||||
<div
|
<div class="relative shrink-0 h-full" style={{ width: `${layout.fileTree.width()}px` }}>
|
||||||
id="file-tree-panel"
|
|
||||||
class="relative shrink-0 h-full"
|
|
||||||
style={{ width: `${layout.fileTree.width()}px` }}
|
|
||||||
>
|
|
||||||
<div class="h-full border-l border-border-weak-base flex flex-col overflow-hidden group/filetree">
|
<div class="h-full border-l border-border-weak-base flex flex-col overflow-hidden group/filetree">
|
||||||
<Tabs
|
<Tabs
|
||||||
variant="pill"
|
variant="pill"
|
||||||
@@ -2864,15 +2658,14 @@ export default function Page() {
|
|||||||
<FileTree
|
<FileTree
|
||||||
path=""
|
path=""
|
||||||
allowed={diffFiles()}
|
allowed={diffFiles()}
|
||||||
kinds={kinds()}
|
|
||||||
draggable={false}
|
draggable={false}
|
||||||
active={activeDiff()}
|
tooltip={false}
|
||||||
onFileClick={(node) => focusReviewDiff(node.path)}
|
onFileClick={(node) => focusReviewDiff(node.path)}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<div class="mt-8 text-center text-12-regular text-text-weak">
|
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||||
{language.t("session.review.noChanges")}
|
{language.t("session.review.noChanges")}
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -2882,7 +2675,7 @@ export default function Page() {
|
|||||||
<FileTree
|
<FileTree
|
||||||
path=""
|
path=""
|
||||||
modified={diffFiles()}
|
modified={diffFiles()}
|
||||||
kinds={kinds()}
|
tooltip={false}
|
||||||
onFileClick={(node) => openTab(file.tab(node.path))}
|
onFileClick={(node) => openTab(file.tab(node.path))}
|
||||||
/>
|
/>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
const defaults: Record<string, string> = {
|
|
||||||
ask: "var(--icon-agent-ask-base)",
|
|
||||||
build: "var(--icon-agent-build-base)",
|
|
||||||
docs: "var(--icon-agent-docs-base)",
|
|
||||||
plan: "var(--icon-agent-plan-base)",
|
|
||||||
}
|
|
||||||
|
|
||||||
export function agentColor(name: string, custom?: string) {
|
|
||||||
if (custom) return custom
|
|
||||||
return defaults[name] ?? defaults[name.toLowerCase()]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
|
||||||
|
|
||||||
export function decode64(value: string | undefined) {
|
|
||||||
if (value === undefined) return
|
|
||||||
try {
|
|
||||||
return base64Decode(value)
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,52 +18,7 @@ const LEGACY_STORAGE = "default.dat"
|
|||||||
const GLOBAL_STORAGE = "opencode.global.dat"
|
const GLOBAL_STORAGE = "opencode.global.dat"
|
||||||
const LOCAL_PREFIX = "opencode."
|
const LOCAL_PREFIX = "opencode."
|
||||||
const fallback = { disabled: false }
|
const fallback = { disabled: false }
|
||||||
|
const cache = new Map<string, string>()
|
||||||
const CACHE_MAX_ENTRIES = 500
|
|
||||||
const CACHE_MAX_BYTES = 8 * 1024 * 1024
|
|
||||||
|
|
||||||
type CacheEntry = { value: string; bytes: number }
|
|
||||||
const cache = new Map<string, CacheEntry>()
|
|
||||||
const cacheTotal = { bytes: 0 }
|
|
||||||
|
|
||||||
function cacheDelete(key: string) {
|
|
||||||
const entry = cache.get(key)
|
|
||||||
if (!entry) return
|
|
||||||
cacheTotal.bytes -= entry.bytes
|
|
||||||
cache.delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cachePrune() {
|
|
||||||
for (;;) {
|
|
||||||
if (cache.size <= CACHE_MAX_ENTRIES && cacheTotal.bytes <= CACHE_MAX_BYTES) return
|
|
||||||
const oldest = cache.keys().next().value as string | undefined
|
|
||||||
if (!oldest) return
|
|
||||||
cacheDelete(oldest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cacheSet(key: string, value: string) {
|
|
||||||
const bytes = value.length * 2
|
|
||||||
if (bytes > CACHE_MAX_BYTES) {
|
|
||||||
cacheDelete(key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = cache.get(key)
|
|
||||||
if (entry) cacheTotal.bytes -= entry.bytes
|
|
||||||
cache.delete(key)
|
|
||||||
cache.set(key, { value, bytes })
|
|
||||||
cacheTotal.bytes += bytes
|
|
||||||
cachePrune()
|
|
||||||
}
|
|
||||||
|
|
||||||
function cacheGet(key: string) {
|
|
||||||
const entry = cache.get(key)
|
|
||||||
if (!entry) return
|
|
||||||
cache.delete(key)
|
|
||||||
cache.set(key, entry)
|
|
||||||
return entry.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function quota(error: unknown) {
|
function quota(error: unknown) {
|
||||||
if (error instanceof DOMException) {
|
if (error instanceof DOMException) {
|
||||||
@@ -108,11 +63,9 @@ function evict(storage: Storage, keep: string, value: string) {
|
|||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
storage.removeItem(item.key)
|
storage.removeItem(item.key)
|
||||||
cacheDelete(item.key)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
storage.setItem(keep, value)
|
storage.setItem(keep, value)
|
||||||
cacheSet(keep, value)
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!quota(error)) throw error
|
if (!quota(error)) throw error
|
||||||
@@ -125,7 +78,6 @@ function evict(storage: Storage, keep: string, value: string) {
|
|||||||
function write(storage: Storage, key: string, value: string) {
|
function write(storage: Storage, key: string, value: string) {
|
||||||
try {
|
try {
|
||||||
storage.setItem(key, value)
|
storage.setItem(key, value)
|
||||||
cacheSet(key, value)
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!quota(error)) throw error
|
if (!quota(error)) throw error
|
||||||
@@ -133,17 +85,13 @@ function write(storage: Storage, key: string, value: string) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
storage.removeItem(key)
|
storage.removeItem(key)
|
||||||
cacheDelete(key)
|
|
||||||
storage.setItem(key, value)
|
storage.setItem(key, value)
|
||||||
cacheSet(key, value)
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!quota(error)) throw error
|
if (!quota(error)) throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = evict(storage, key, value)
|
return evict(storage, key, value)
|
||||||
if (!ok) cacheSet(key, value)
|
|
||||||
return ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapshot(value: unknown) {
|
function snapshot(value: unknown) {
|
||||||
@@ -200,24 +148,17 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
|
|||||||
return {
|
return {
|
||||||
getItem: (key) => {
|
getItem: (key) => {
|
||||||
const name = item(key)
|
const name = item(key)
|
||||||
const cached = cacheGet(name)
|
const cached = cache.get(name)
|
||||||
if (fallback.disabled && cached !== undefined) return cached
|
if (fallback.disabled && cached !== undefined) return cached
|
||||||
|
|
||||||
const stored = (() => {
|
const stored = localStorage.getItem(name)
|
||||||
try {
|
|
||||||
return localStorage.getItem(name)
|
|
||||||
} catch {
|
|
||||||
fallback.disabled = true
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
if (stored === null) return cached ?? null
|
if (stored === null) return cached ?? null
|
||||||
cacheSet(name, stored)
|
cache.set(name, stored)
|
||||||
return stored
|
return stored
|
||||||
},
|
},
|
||||||
setItem: (key, value) => {
|
setItem: (key, value) => {
|
||||||
const name = item(key)
|
const name = item(key)
|
||||||
cacheSet(name, value)
|
cache.set(name, value)
|
||||||
if (fallback.disabled) return
|
if (fallback.disabled) return
|
||||||
try {
|
try {
|
||||||
if (write(localStorage, name, value)) return
|
if (write(localStorage, name, value)) return
|
||||||
@@ -229,13 +170,9 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
|
|||||||
},
|
},
|
||||||
removeItem: (key) => {
|
removeItem: (key) => {
|
||||||
const name = item(key)
|
const name = item(key)
|
||||||
cacheDelete(name)
|
cache.delete(name)
|
||||||
if (fallback.disabled) return
|
if (fallback.disabled) return
|
||||||
try {
|
localStorage.removeItem(name)
|
||||||
localStorage.removeItem(name)
|
|
||||||
} catch {
|
|
||||||
fallback.disabled = true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,23 +180,16 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
|
|||||||
function localStorageDirect(): SyncStorage {
|
function localStorageDirect(): SyncStorage {
|
||||||
return {
|
return {
|
||||||
getItem: (key) => {
|
getItem: (key) => {
|
||||||
const cached = cacheGet(key)
|
const cached = cache.get(key)
|
||||||
if (fallback.disabled && cached !== undefined) return cached
|
if (fallback.disabled && cached !== undefined) return cached
|
||||||
|
|
||||||
const stored = (() => {
|
const stored = localStorage.getItem(key)
|
||||||
try {
|
|
||||||
return localStorage.getItem(key)
|
|
||||||
} catch {
|
|
||||||
fallback.disabled = true
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
if (stored === null) return cached ?? null
|
if (stored === null) return cached ?? null
|
||||||
cacheSet(key, stored)
|
cache.set(key, stored)
|
||||||
return stored
|
return stored
|
||||||
},
|
},
|
||||||
setItem: (key, value) => {
|
setItem: (key, value) => {
|
||||||
cacheSet(key, value)
|
cache.set(key, value)
|
||||||
if (fallback.disabled) return
|
if (fallback.disabled) return
|
||||||
try {
|
try {
|
||||||
if (write(localStorage, key, value)) return
|
if (write(localStorage, key, value)) return
|
||||||
@@ -270,13 +200,9 @@ function localStorageDirect(): SyncStorage {
|
|||||||
fallback.disabled = true
|
fallback.disabled = true
|
||||||
},
|
},
|
||||||
removeItem: (key) => {
|
removeItem: (key) => {
|
||||||
cacheDelete(key)
|
cache.delete(key)
|
||||||
if (fallback.disabled) return
|
if (fallback.disabled) return
|
||||||
try {
|
localStorage.removeItem(key)
|
||||||
localStorage.removeItem(key)
|
|
||||||
} catch {
|
|
||||||
fallback.disabled = true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export function createSpeechRecognition(opts?: {
|
|||||||
let lastInterimSuffix = ""
|
let lastInterimSuffix = ""
|
||||||
let shrinkCandidate: string | undefined
|
let shrinkCandidate: string | undefined
|
||||||
let commitTimer: number | undefined
|
let commitTimer: number | undefined
|
||||||
let restartTimer: number | undefined
|
|
||||||
|
|
||||||
const cancelPendingCommit = () => {
|
const cancelPendingCommit = () => {
|
||||||
if (commitTimer === undefined) return
|
if (commitTimer === undefined) return
|
||||||
@@ -86,26 +85,6 @@ export function createSpeechRecognition(opts?: {
|
|||||||
commitTimer = undefined
|
commitTimer = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearRestart = () => {
|
|
||||||
if (restartTimer === undefined) return
|
|
||||||
window.clearTimeout(restartTimer)
|
|
||||||
restartTimer = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleRestart = () => {
|
|
||||||
clearRestart()
|
|
||||||
if (!shouldContinue) return
|
|
||||||
if (!recognition) return
|
|
||||||
restartTimer = window.setTimeout(() => {
|
|
||||||
restartTimer = undefined
|
|
||||||
if (!shouldContinue) return
|
|
||||||
if (!recognition) return
|
|
||||||
try {
|
|
||||||
recognition.start()
|
|
||||||
} catch {}
|
|
||||||
}, 150)
|
|
||||||
}
|
|
||||||
|
|
||||||
const commitSegment = (segment: string) => {
|
const commitSegment = (segment: string) => {
|
||||||
const nextCommitted = appendSegment(committedText, segment)
|
const nextCommitted = appendSegment(committedText, segment)
|
||||||
if (nextCommitted === committedText) return
|
if (nextCommitted === committedText) return
|
||||||
@@ -235,14 +214,17 @@ export function createSpeechRecognition(opts?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recognition.onerror = (e: { error: string }) => {
|
recognition.onerror = (e: { error: string }) => {
|
||||||
clearRestart()
|
|
||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
if (e.error === "no-speech" && shouldContinue) {
|
if (e.error === "no-speech" && shouldContinue) {
|
||||||
setStore("interim", "")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
scheduleRestart()
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
recognition?.start()
|
||||||
|
} catch {}
|
||||||
|
}, 150)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
shouldContinue = false
|
shouldContinue = false
|
||||||
@@ -250,7 +232,6 @@ export function createSpeechRecognition(opts?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recognition.onstart = () => {
|
recognition.onstart = () => {
|
||||||
clearRestart()
|
|
||||||
sessionCommitted = ""
|
sessionCommitted = ""
|
||||||
pendingHypothesis = ""
|
pendingHypothesis = ""
|
||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
@@ -262,20 +243,22 @@ export function createSpeechRecognition(opts?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recognition.onend = () => {
|
recognition.onend = () => {
|
||||||
clearRestart()
|
|
||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setStore("isRecording", false)
|
setStore("isRecording", false)
|
||||||
if (shouldContinue) {
|
if (shouldContinue) {
|
||||||
scheduleRestart()
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
recognition?.start()
|
||||||
|
} catch {}
|
||||||
|
}, 150)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
if (!recognition) return
|
if (!recognition) return
|
||||||
clearRestart()
|
|
||||||
shouldContinue = true
|
shouldContinue = true
|
||||||
sessionCommitted = ""
|
sessionCommitted = ""
|
||||||
pendingHypothesis = ""
|
pendingHypothesis = ""
|
||||||
@@ -291,7 +274,6 @@ export function createSpeechRecognition(opts?: {
|
|||||||
const stop = () => {
|
const stop = () => {
|
||||||
if (!recognition) return
|
if (!recognition) return
|
||||||
shouldContinue = false
|
shouldContinue = false
|
||||||
clearRestart()
|
|
||||||
promotePending()
|
promotePending()
|
||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
@@ -305,7 +287,6 @@ export function createSpeechRecognition(opts?: {
|
|||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
shouldContinue = false
|
shouldContinue = false
|
||||||
clearRestart()
|
|
||||||
promotePending()
|
promotePending()
|
||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
|
|||||||
@@ -13,21 +13,7 @@ type State =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = new Map<string, State>()
|
const state = new Map<string, State>()
|
||||||
const waiters = new Map<
|
const waiters = new Map<string, Array<(state: State) => void>>()
|
||||||
string,
|
|
||||||
{
|
|
||||||
promise: Promise<State>
|
|
||||||
resolve: (state: State) => void
|
|
||||||
}
|
|
||||||
>()
|
|
||||||
|
|
||||||
function deferred() {
|
|
||||||
const box = { resolve: (_: State) => {} }
|
|
||||||
const promise = new Promise<State>((resolve) => {
|
|
||||||
box.resolve = resolve
|
|
||||||
})
|
|
||||||
return { promise, resolve: box.resolve }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Worktree = {
|
export const Worktree = {
|
||||||
get(directory: string) {
|
get(directory: string) {
|
||||||
@@ -41,33 +27,32 @@ export const Worktree = {
|
|||||||
},
|
},
|
||||||
ready(directory: string) {
|
ready(directory: string) {
|
||||||
const key = normalize(directory)
|
const key = normalize(directory)
|
||||||
const next = { status: "ready" } as const
|
state.set(key, { status: "ready" })
|
||||||
state.set(key, next)
|
const list = waiters.get(key)
|
||||||
const waiter = waiters.get(key)
|
if (!list) return
|
||||||
if (!waiter) return
|
|
||||||
waiters.delete(key)
|
waiters.delete(key)
|
||||||
waiter.resolve(next)
|
for (const fn of list) fn({ status: "ready" })
|
||||||
},
|
},
|
||||||
failed(directory: string, message: string) {
|
failed(directory: string, message: string) {
|
||||||
const key = normalize(directory)
|
const key = normalize(directory)
|
||||||
const next = { status: "failed", message } as const
|
state.set(key, { status: "failed", message })
|
||||||
state.set(key, next)
|
const list = waiters.get(key)
|
||||||
const waiter = waiters.get(key)
|
if (!list) return
|
||||||
if (!waiter) return
|
|
||||||
waiters.delete(key)
|
waiters.delete(key)
|
||||||
waiter.resolve(next)
|
for (const fn of list) fn({ status: "failed", message })
|
||||||
},
|
},
|
||||||
wait(directory: string) {
|
wait(directory: string) {
|
||||||
const key = normalize(directory)
|
const key = normalize(directory)
|
||||||
const current = state.get(key)
|
const current = state.get(key)
|
||||||
if (current && current.status !== "pending") return Promise.resolve(current)
|
if (current && current.status !== "pending") return Promise.resolve(current)
|
||||||
|
|
||||||
const existing = waiters.get(key)
|
return new Promise<State>((resolve) => {
|
||||||
if (existing) return existing.promise
|
const list = waiters.get(key)
|
||||||
|
if (!list) {
|
||||||
const waiter = deferred()
|
waiters.set(key, [resolve])
|
||||||
|
return
|
||||||
waiters.set(key, waiter)
|
}
|
||||||
return waiter.promise
|
list.push(resolve)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0-ci-202601291718",
|
"version": "1.1.36",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -15,10 +15,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@solid-primitives/i18n": "2.2.1",
|
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-deep-link": "~2",
|
|
||||||
"@tauri-apps/plugin-dialog": "~2",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-os": "~2",
|
"@tauri-apps/plugin-os": "~2",
|
||||||
|
|||||||
Generated
+4
-102
@@ -609,26 +609,6 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "const-random"
|
|
||||||
version = "0.1.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
|
||||||
dependencies = [
|
|
||||||
"const-random-macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "const-random-macro"
|
|
||||||
version = "0.1.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.16",
|
|
||||||
"once_cell",
|
|
||||||
"tiny-keccak",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -1000,15 +980,6 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dlv-list"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
|
||||||
dependencies = [
|
|
||||||
"const-random",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@@ -1806,12 +1777,6 @@ version = "0.12.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.14.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -1965,7 +1930,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-registry 0.6.1",
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2380,7 +2345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a4f8240c33bb08c5d8b8cdea87b683b05e61037aa76ff26bef40672cc6ecbb80"
|
checksum = "a4f8240c33bb08c5d8b8cdea87b683b05e61037aa76ff26bef40672cc6ecbb80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"freedesktop_entry_parser",
|
"freedesktop_entry_parser",
|
||||||
"rust-ini 0.17.0",
|
"rust-ini",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3073,7 +3038,6 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
"tauri-plugin-decorum",
|
"tauri-plugin-decorum",
|
||||||
"tauri-plugin-deep-link",
|
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
@@ -3103,20 +3067,10 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485"
|
checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dlv-list 0.2.3",
|
"dlv-list",
|
||||||
"hashbrown 0.9.1",
|
"hashbrown 0.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ordered-multimap"
|
|
||||||
version = "0.7.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
|
||||||
dependencies = [
|
|
||||||
"dlv-list 0.5.2",
|
|
||||||
"hashbrown 0.14.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3993,17 +3947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22"
|
checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"ordered-multimap 0.3.1",
|
"ordered-multimap",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust-ini"
|
|
||||||
version = "0.21.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"ordered-multimap 0.7.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4873,27 +4817,6 @@ dependencies = [
|
|||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tauri-plugin-deep-link"
|
|
||||||
version = "2.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "444b091f24f2f6bdb4a305b54d3961f629c11861c685aceeea9a1972f89e43d5"
|
|
||||||
dependencies = [
|
|
||||||
"dunce",
|
|
||||||
"plist",
|
|
||||||
"rust-ini 0.21.3",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tauri",
|
|
||||||
"tauri-plugin",
|
|
||||||
"tauri-utils",
|
|
||||||
"thiserror 2.0.17",
|
|
||||||
"tracing",
|
|
||||||
"url",
|
|
||||||
"windows-registry 0.5.3",
|
|
||||||
"windows-result 0.3.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.4.2"
|
version = "2.4.2"
|
||||||
@@ -5057,7 +4980,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin-deep-link",
|
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
@@ -5349,15 +5271,6 @@ dependencies = [
|
|||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tiny-keccak"
|
|
||||||
version = "2.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
|
||||||
dependencies = [
|
|
||||||
"crunchy",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -6295,17 +6208,6 @@ dependencies = [
|
|||||||
"windows-link 0.1.3",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-registry"
|
|
||||||
version = "0.5.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link 0.1.3",
|
|
||||||
"windows-result 0.3.4",
|
|
||||||
"windows-strings 0.4.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-registry"
|
name = "windows-registry"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ tauri-build = { version = "2", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = ["macos-private-api", "devtools"] }
|
tauri = { version = "2", features = ["macos-private-api", "devtools"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-deep-link = "2.4.6"
|
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
@@ -30,7 +29,7 @@ tauri-plugin-window-state = "2"
|
|||||||
tauri-plugin-clipboard-manager = "2"
|
tauri-plugin-clipboard-manager = "2"
|
||||||
tauri-plugin-http = "2"
|
tauri-plugin-http = "2"
|
||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
tauri-plugin-single-instance = "2"
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"deep-link:default",
|
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
"core:webview:allow-set-webview-zoom",
|
"core:webview:allow-set-webview-zoom",
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ use std::{
|
|||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
|
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
|
||||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use tauri_plugin_decorum::WebviewWindowExt;
|
use tauri_plugin_decorum::WebviewWindowExt;
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
||||||
@@ -265,7 +263,6 @@ pub fn run() {
|
|||||||
let _ = window.unminimize();
|
let _ = window.unminimize();
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_window_state::Builder::new()
|
tauri_plugin_window_state::Builder::new()
|
||||||
@@ -294,9 +291,6 @@ pub fn run() {
|
|||||||
markdown::parse_markdown_command
|
markdown::parse_markdown_command
|
||||||
])
|
])
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
|
||||||
app.deep_link().register_all().ok();
|
|
||||||
|
|
||||||
let app = app.handle().clone();
|
let app = app.handle().clone();
|
||||||
|
|
||||||
// Initialize log state
|
// Initialize log state
|
||||||
@@ -334,15 +328,7 @@ pub fn run() {
|
|||||||
.hidden_title(true);
|
.hidden_title(true);
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let window_builder = window_builder
|
let window_builder = window_builder.decorations(false);
|
||||||
// Some VPNs set a global/system proxy that WebView2 applies even for loopback
|
|
||||||
// connections, which breaks the app's localhost sidecar server.
|
|
||||||
// Note: when setting additional args, we must re-apply wry's default
|
|
||||||
// `--disable-features=...` flags.
|
|
||||||
.additional_browser_args(
|
|
||||||
"--proxy-bypass-list=<-loopback> --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection",
|
|
||||||
)
|
|
||||||
.decorations(false);
|
|
||||||
|
|
||||||
let window = window_builder.build().expect("Failed to create window");
|
let window = window_builder.build().expect("Failed to create window");
|
||||||
|
|
||||||
@@ -539,4 +525,4 @@ async fn spawn_local_server(
|
|||||||
break Ok(child);
|
break Ok(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,32 +52,7 @@ fn configure_display_backend() -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Ensure loopback connections are never sent through proxy settings.
|
unsafe { std::env::set_var("NO_PROXY", "127.0.0.1,localhost,::1") };
|
||||||
// Some VPNs/proxies set HTTP_PROXY/HTTPS_PROXY/ALL_PROXY without excluding localhost.
|
|
||||||
const LOOPBACK: [&str; 3] = ["127.0.0.1", "localhost", "::1"];
|
|
||||||
|
|
||||||
let upsert = |key: &str| {
|
|
||||||
let mut items = std::env::var(key)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.split(',')
|
|
||||||
.map(|v| v.trim())
|
|
||||||
.filter(|v| !v.is_empty())
|
|
||||||
.map(|v| v.to_string())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
for host in LOOPBACK {
|
|
||||||
if items.iter().any(|v| v.eq_ignore_ascii_case(host)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
items.push(host.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safety: called during startup before any threads are spawned.
|
|
||||||
unsafe { std::env::set_var(key, items.join(",")) };
|
|
||||||
};
|
|
||||||
|
|
||||||
upsert("NO_PROXY");
|
|
||||||
upsert("no_proxy");
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,43 +1,4 @@
|
|||||||
use comrak::{create_formatter, parse_document, Arena, Options, html::ChildRendering, nodes::NodeValue};
|
use comrak::{markdown_to_html, Options};
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
create_formatter!(ExternalLinkFormatter, {
|
|
||||||
NodeValue::Link(ref nl) => |context, node, entering| {
|
|
||||||
let skip = context.options.parse.relaxed_autolinks
|
|
||||||
&& node.parent().is_some_and(|p| comrak::node_matches!(p, NodeValue::Link(..)));
|
|
||||||
if skip {
|
|
||||||
return Ok(ChildRendering::HTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
if entering {
|
|
||||||
context.write_str("<a")?;
|
|
||||||
comrak::html::render_sourcepos(context, node)?;
|
|
||||||
|
|
||||||
context.write_str(" href=\"")?;
|
|
||||||
let url = &nl.url;
|
|
||||||
if context.options.render.r#unsafe || !comrak::html::dangerous_url(url) {
|
|
||||||
if let Some(rewriter) = &context.options.extension.link_url_rewriter {
|
|
||||||
context.escape_href(&rewriter.to_html(url))?;
|
|
||||||
} else {
|
|
||||||
context.escape_href(url)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.write_str("\"")?;
|
|
||||||
|
|
||||||
if !nl.title.is_empty() {
|
|
||||||
context.write_str(" title=\"")?;
|
|
||||||
context.escape(&nl.title)?;
|
|
||||||
context.write_str("\"")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.write_str(
|
|
||||||
" class=\"external-link\" target=\"_blank\" rel=\"noopener noreferrer\">",
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
context.write_str("</a>")?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
pub fn parse_markdown(input: &str) -> String {
|
pub fn parse_markdown(input: &str) -> String {
|
||||||
let mut options = Options::default();
|
let mut options = Options::default();
|
||||||
@@ -47,11 +8,7 @@ pub fn parse_markdown(input: &str) -> String {
|
|||||||
options.extension.autolink = true;
|
options.extension.autolink = true;
|
||||||
options.render.r#unsafe = true;
|
options.render.r#unsafe = true;
|
||||||
|
|
||||||
let arena = Arena::new();
|
markdown_to_html(input, &options)
|
||||||
let doc = parse_document(&arena, input, &options);
|
|
||||||
let mut html = String::new();
|
|
||||||
ExternalLinkFormatter::format_document(doc, &options, &mut html).unwrap_or_default();
|
|
||||||
html
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -52,12 +52,5 @@
|
|||||||
"sidebarImage": "assets/nsis-sidebar.bmp"
|
"sidebarImage": "assets/nsis-sidebar.bmp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"deep-link": {
|
|
||||||
"desktop": {
|
|
||||||
"schemes": ["opencode"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,27 @@
|
|||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "OpenCode",
|
"productName": "OpenCode",
|
||||||
"identifier": "ai.opencode.desktop",
|
"identifier": "ai.opencode.desktop",
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "main",
|
||||||
|
"create": false,
|
||||||
|
"title": "OpenCode",
|
||||||
|
"url": "/",
|
||||||
|
"decorations": true,
|
||||||
|
"dragDropEnabled": false,
|
||||||
|
"zoomHotkeysEnabled": true,
|
||||||
|
"titleBarStyle": "Overlay",
|
||||||
|
"hiddenTitle": true,
|
||||||
|
"trafficLightPosition": { "x": 12.0, "y": 18.0 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"withGlobalTauri": true,
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"macOSPrivateApi": true
|
||||||
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"createUpdaterArtifacts": true,
|
"createUpdaterArtifacts": true,
|
||||||
"icon": [
|
"icon": [
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core"
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
import { message } from "@tauri-apps/plugin-dialog"
|
import { message } from "@tauri-apps/plugin-dialog"
|
||||||
|
|
||||||
import { initI18n, t } from "./i18n"
|
|
||||||
|
|
||||||
export async function installCli(): Promise<void> {
|
export async function installCli(): Promise<void> {
|
||||||
await initI18n()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const path = await invoke<string>("install_cli")
|
const path = await invoke<string>("install_cli")
|
||||||
await message(t("desktop.cli.installed.message", { path }), { title: t("desktop.cli.installed.title") })
|
await message(`CLI installed to ${path}\n\nRestart your terminal to use the 'opencode' command.`, {
|
||||||
|
title: "CLI Installed",
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await message(t("desktop.cli.failed.message", { error: String(e) }), { title: t("desktop.cli.failed.title") })
|
await message(`Failed to install CLI: ${e}`, { title: "Installation Failed" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"desktop.menu.checkForUpdates": "التحقق من وجود تحديثات...",
|
|
||||||
"desktop.menu.installCli": "تثبيت CLI...",
|
|
||||||
"desktop.menu.reloadWebview": "إعادة تحميل Webview",
|
|
||||||
"desktop.menu.restart": "إعادة تشغيل",
|
|
||||||
|
|
||||||
"desktop.dialog.chooseFolder": "اختر مجلدًا",
|
|
||||||
"desktop.dialog.chooseFile": "اختر ملفًا",
|
|
||||||
"desktop.dialog.saveFile": "حفظ ملف",
|
|
||||||
|
|
||||||
"desktop.updater.checkFailed.title": "فشل التحقق من التحديثات",
|
|
||||||
"desktop.updater.checkFailed.message": "فشل التحقق من وجود تحديثات",
|
|
||||||
"desktop.updater.none.title": "لا توجد تحديثات متاحة",
|
|
||||||
"desktop.updater.none.message": "أنت تستخدم بالفعل أحدث إصدار من OpenCode",
|
|
||||||
"desktop.updater.downloadFailed.title": "فشل التحديث",
|
|
||||||
"desktop.updater.downloadFailed.message": "فشل تنزيل التحديث",
|
|
||||||
"desktop.updater.downloaded.title": "تم تنزيل التحديث",
|
|
||||||
"desktop.updater.downloaded.prompt": "تم تنزيل إصدار {{version}} من OpenCode، هل ترغب في تثبيته وإعادة تشغيله؟",
|
|
||||||
"desktop.updater.installFailed.title": "فشل التحديث",
|
|
||||||
"desktop.updater.installFailed.message": "فشل تثبيت التحديث",
|
|
||||||
|
|
||||||
"desktop.cli.installed.title": "تم تثبيت CLI",
|
|
||||||
"desktop.cli.installed.message": "تم تثبيت CLI في {{path}}\n\nأعد تشغيل الطرفية لاستخدام الأمر 'opencode'.",
|
|
||||||
"desktop.cli.failed.title": "فشل التثبيت",
|
|
||||||
"desktop.cli.failed.message": "فشل تثبيت CLI: {{error}}",
|
|
||||||
|
|
||||||
"desktop.error.serverStartFailed.title": "فشل تشغيل OpenCode",
|
|
||||||
"desktop.error.serverStartFailed.description":
|
|
||||||
"تعذر بدء تشغيل خادم OpenCode المحلي. أعد تشغيل التطبيق، أو تحقق من إعدادات الشبكة (VPN/proxy) وحاول مرة أخرى.",
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"desktop.menu.checkForUpdates": "Verificar atualizações...",
|
|
||||||
"desktop.menu.installCli": "Instalar CLI...",
|
|
||||||
"desktop.menu.reloadWebview": "Recarregar Webview",
|
|
||||||
"desktop.menu.restart": "Reiniciar",
|
|
||||||
|
|
||||||
"desktop.dialog.chooseFolder": "Escolher uma pasta",
|
|
||||||
"desktop.dialog.chooseFile": "Escolher um arquivo",
|
|
||||||
"desktop.dialog.saveFile": "Salvar arquivo",
|
|
||||||
|
|
||||||
"desktop.updater.checkFailed.title": "Falha ao verificar atualizações",
|
|
||||||
"desktop.updater.checkFailed.message": "Falha ao verificar atualizações",
|
|
||||||
"desktop.updater.none.title": "Nenhuma atualização disponível",
|
|
||||||
"desktop.updater.none.message": "Você já está usando a versão mais recente do OpenCode",
|
|
||||||
"desktop.updater.downloadFailed.title": "Falha na atualização",
|
|
||||||
"desktop.updater.downloadFailed.message": "Falha ao baixar a atualização",
|
|
||||||
"desktop.updater.downloaded.title": "Atualização baixada",
|
|
||||||
"desktop.updater.downloaded.prompt":
|
|
||||||
"A versão {{version}} do OpenCode foi baixada. Você gostaria de instalá-la e reiniciar?",
|
|
||||||
"desktop.updater.installFailed.title": "Falha na atualização",
|
|
||||||
"desktop.updater.installFailed.message": "Falha ao instalar a atualização",
|
|
||||||
|
|
||||||
"desktop.cli.installed.title": "CLI instalada",
|
|
||||||
"desktop.cli.installed.message": "CLI instalada em {{path}}\n\nReinicie seu terminal para usar o comando 'opencode'.",
|
|
||||||
"desktop.cli.failed.title": "Falha na instalação",
|
|
||||||
"desktop.cli.failed.message": "Falha ao instalar a CLI: {{error}}",
|
|
||||||
|
|
||||||
"desktop.error.serverStartFailed.title": "Falha ao iniciar o OpenCode",
|
|
||||||
"desktop.error.serverStartFailed.description":
|
|
||||||
"Não foi possível iniciar o servidor local do OpenCode. Reinicie o aplicativo ou verifique suas configurações de rede (VPN/proxy) e tente novamente.",
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"desktop.menu.checkForUpdates": "Tjek for opdateringer...",
|
|
||||||
"desktop.menu.installCli": "Installer CLI...",
|
|
||||||
"desktop.menu.reloadWebview": "Genindlæs Webview",
|
|
||||||
"desktop.menu.restart": "Genstart",
|
|
||||||
|
|
||||||
"desktop.dialog.chooseFolder": "Vælg en mappe",
|
|
||||||
"desktop.dialog.chooseFile": "Vælg en fil",
|
|
||||||
"desktop.dialog.saveFile": "Gem fil",
|
|
||||||
|
|
||||||
"desktop.updater.checkFailed.title": "Opdateringstjek mislykkedes",
|
|
||||||
"desktop.updater.checkFailed.message": "Kunne ikke tjekke for opdateringer",
|
|
||||||
"desktop.updater.none.title": "Ingen opdatering tilgængelig",
|
|
||||||
"desktop.updater.none.message": "Du bruger allerede den nyeste version af OpenCode",
|
|
||||||
"desktop.updater.downloadFailed.title": "Opdatering mislykkedes",
|
|
||||||
"desktop.updater.downloadFailed.message": "Kunne ikke downloade opdateringen",
|
|
||||||
"desktop.updater.downloaded.title": "Opdatering downloadet",
|
|
||||||
"desktop.updater.downloaded.prompt":
|
|
||||||
"Version {{version}} af OpenCode er blevet downloadet. Vil du installere den og genstarte?",
|
|
||||||
"desktop.updater.installFailed.title": "Opdatering mislykkedes",
|
|
||||||
"desktop.updater.installFailed.message": "Kunne ikke installere opdateringen",
|
|
||||||
|
|
||||||
"desktop.cli.installed.title": "CLI installeret",
|
|
||||||
"desktop.cli.installed.message":
|
|
||||||
"CLI installeret i {{path}}\n\nGenstart din terminal for at bruge 'opencode'-kommandoen.",
|
|
||||||
"desktop.cli.failed.title": "Installation mislykkedes",
|
|
||||||
"desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}",
|
|
||||||
|
|
||||||
"desktop.error.serverStartFailed.title": "OpenCode kunne ikke starte",
|
|
||||||
"desktop.error.serverStartFailed.description":
|
|
||||||
"Den lokale OpenCode-server kunne ikke startes. Genstart appen, eller tjek dine netværksindstillinger (VPN/proxy) og prøv igen.",
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"desktop.menu.checkForUpdates": "Nach Updates suchen...",
|
|
||||||
"desktop.menu.installCli": "CLI installieren...",
|
|
||||||
"desktop.menu.reloadWebview": "Webview neu laden",
|
|
||||||
"desktop.menu.restart": "Neustart",
|
|
||||||
|
|
||||||
"desktop.dialog.chooseFolder": "Ordner auswählen",
|
|
||||||
"desktop.dialog.chooseFile": "Datei auswählen",
|
|
||||||
"desktop.dialog.saveFile": "Datei speichern",
|
|
||||||
|
|
||||||
"desktop.updater.checkFailed.title": "Updateprüfung fehlgeschlagen",
|
|
||||||
"desktop.updater.checkFailed.message": "Updates konnten nicht geprüft werden",
|
|
||||||
"desktop.updater.none.title": "Kein Update verfügbar",
|
|
||||||
"desktop.updater.none.message": "Sie verwenden bereits die neueste Version von OpenCode",
|
|
||||||
"desktop.updater.downloadFailed.title": "Update fehlgeschlagen",
|
|
||||||
"desktop.updater.downloadFailed.message": "Update konnte nicht heruntergeladen werden",
|
|
||||||
"desktop.updater.downloaded.title": "Update heruntergeladen",
|
|
||||||
"desktop.updater.downloaded.prompt":
|
|
||||||
"Version {{version}} von OpenCode wurde heruntergeladen. Möchten Sie sie installieren und neu starten?",
|
|
||||||
"desktop.updater.installFailed.title": "Update fehlgeschlagen",
|
|
||||||
"desktop.updater.installFailed.message": "Update konnte nicht installiert werden",
|
|
||||||
|
|
||||||
"desktop.cli.installed.title": "CLI installiert",
|
|
||||||
"desktop.cli.installed.message":
|
|
||||||
"CLI wurde in {{path}} installiert\n\nStarten Sie Ihr Terminal neu, um den Befehl 'opencode' zu verwenden.",
|
|
||||||
"desktop.cli.failed.title": "Installation fehlgeschlagen",
|
|
||||||
"desktop.cli.failed.message": "CLI konnte nicht installiert werden: {{error}}",
|
|
||||||
|
|
||||||
"desktop.error.serverStartFailed.title": "OpenCode konnte nicht gestartet werden",
|
|
||||||
"desktop.error.serverStartFailed.description":
|
|
||||||
"Der lokale OpenCode-Server konnte nicht gestartet werden. Starten Sie die App neu oder überprüfen Sie Ihre Netzwerkeinstellungen (VPN/Proxy) und versuchen Sie es erneut.",
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"desktop.menu.checkForUpdates": "Check for Updates...",
|
|
||||||
"desktop.menu.installCli": "Install CLI...",
|
|
||||||
"desktop.menu.reloadWebview": "Reload Webview",
|
|
||||||
"desktop.menu.restart": "Restart",
|
|
||||||
|
|
||||||
"desktop.dialog.chooseFolder": "Choose a folder",
|
|
||||||
"desktop.dialog.chooseFile": "Choose a file",
|
|
||||||
"desktop.dialog.saveFile": "Save file",
|
|
||||||
|
|
||||||
"desktop.updater.checkFailed.title": "Update Check Failed",
|
|
||||||
"desktop.updater.checkFailed.message": "Failed to check for updates",
|
|
||||||
"desktop.updater.none.title": "No Update Available",
|
|
||||||
"desktop.updater.none.message": "You are already using the latest version of OpenCode",
|
|
||||||
"desktop.updater.downloadFailed.title": "Update Failed",
|
|
||||||
"desktop.updater.downloadFailed.message": "Failed to download update",
|
|
||||||
"desktop.updater.downloaded.title": "Update Downloaded",
|
|
||||||
"desktop.updater.downloaded.prompt":
|
|
||||||
"Version {{version}} of OpenCode has been downloaded, would you like to install it and relaunch?",
|
|
||||||
"desktop.updater.installFailed.title": "Update Failed",
|
|
||||||
"desktop.updater.installFailed.message": "Failed to install update",
|
|
||||||
|
|
||||||
"desktop.cli.installed.title": "CLI Installed",
|
|
||||||
"desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode' command.",
|
|
||||||
"desktop.cli.failed.title": "Installation Failed",
|
|
||||||
"desktop.cli.failed.message": "Failed to install CLI: {{error}}",
|
|
||||||
|
|
||||||
"desktop.error.serverStartFailed.title": "OpenCode failed to start",
|
|
||||||
"desktop.error.serverStartFailed.description":
|
|
||||||
"The local OpenCode server could not be started. Restart the app, or check your network settings (VPN/proxy) and try again.",
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"desktop.menu.checkForUpdates": "Buscar actualizaciones...",
|
|
||||||
"desktop.menu.installCli": "Instalar CLI...",
|
|
||||||
"desktop.menu.reloadWebview": "Recargar Webview",
|
|
||||||
"desktop.menu.restart": "Reiniciar",
|
|
||||||
|
|
||||||
"desktop.dialog.chooseFolder": "Elegir una carpeta",
|
|
||||||
"desktop.dialog.chooseFile": "Elegir un archivo",
|
|
||||||
"desktop.dialog.saveFile": "Guardar archivo",
|
|
||||||
|
|
||||||
"desktop.updater.checkFailed.title": "Comprobación de actualizaciones fallida",
|
|
||||||
"desktop.updater.checkFailed.message": "No se pudieron buscar actualizaciones",
|
|
||||||
"desktop.updater.none.title": "No hay actualizaciones disponibles",
|
|
||||||
"desktop.updater.none.message": "Ya estás usando la versión más reciente de OpenCode",
|
|
||||||
"desktop.updater.downloadFailed.title": "Actualización fallida",
|
|
||||||
"desktop.updater.downloadFailed.message": "No se pudo descargar la actualización",
|
|
||||||
"desktop.updater.downloaded.title": "Actualización descargada",
|
|
||||||
"desktop.updater.downloaded.prompt":
|
|
||||||
"Se ha descargado la versión {{version}} de OpenCode. ¿Quieres instalarla y reiniciar?",
|
|
||||||
"desktop.updater.installFailed.title": "Actualización fallida",
|
|
||||||
"desktop.updater.installFailed.message": "No se pudo instalar la actualización",
|
|
||||||
|
|
||||||
"desktop.cli.installed.title": "CLI instalada",
|
|
||||||
"desktop.cli.installed.message": "CLI instalada en {{path}}\n\nReinicia tu terminal para usar el comando 'opencode'.",
|
|
||||||
"desktop.cli.failed.title": "Instalación fallida",
|
|
||||||
"desktop.cli.failed.message": "No se pudo instalar la CLI: {{error}}",
|
|
||||||
|
|
||||||
"desktop.error.serverStartFailed.title": "OpenCode no pudo iniciarse",
|
|
||||||
"desktop.error.serverStartFailed.description":
|
|
||||||
"No se pudo iniciar el servidor local de OpenCode. Reinicia la aplicación o revisa tu configuración de red (VPN/proxy) y vuelve a intentarlo.",
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"desktop.menu.checkForUpdates": "Vérifier les mises à jour...",
|
|
||||||
"desktop.menu.installCli": "Installer la CLI...",
|
|
||||||
"desktop.menu.reloadWebview": "Recharger la Webview",
|
|
||||||
"desktop.menu.restart": "Redémarrer",
|
|
||||||
|
|
||||||
"desktop.dialog.chooseFolder": "Choisir un dossier",
|
|
||||||
"desktop.dialog.chooseFile": "Choisir un fichier",
|
|
||||||
"desktop.dialog.saveFile": "Enregistrer le fichier",
|
|
||||||
|
|
||||||
"desktop.updater.checkFailed.title": "Échec de la vérification des mises à jour",
|
|
||||||
"desktop.updater.checkFailed.message": "Impossible de vérifier les mises à jour",
|
|
||||||
"desktop.updater.none.title": "Aucune mise à jour disponible",
|
|
||||||
"desktop.updater.none.message": "Vous utilisez déjà la dernière version d'OpenCode",
|
|
||||||
"desktop.updater.downloadFailed.title": "Échec de la mise à jour",
|
|
||||||
"desktop.updater.downloadFailed.message": "Impossible de télécharger la mise à jour",
|
|
||||||
"desktop.updater.downloaded.title": "Mise à jour téléchargée",
|
|
||||||
"desktop.updater.downloaded.prompt":
|
|
||||||
"La version {{version}} d'OpenCode a été téléchargée. Voulez-vous l'installer et redémarrer ?",
|
|
||||||
"desktop.updater.installFailed.title": "Échec de la mise à jour",
|
|
||||||
"desktop.updater.installFailed.message": "Impossible d'installer la mise à jour",
|
|
||||||
|
|
||||||
"desktop.cli.installed.title": "CLI installée",
|
|
||||||
"desktop.cli.installed.message":
|
|
||||||
"CLI installée dans {{path}}\n\nRedémarrez votre terminal pour utiliser la commande 'opencode'.",
|
|
||||||
"desktop.cli.failed.title": "Échec de l'installation",
|
|
||||||
"desktop.cli.failed.message": "Impossible d'installer la CLI : {{error}}",
|
|
||||||
|
|
||||||
"desktop.error.serverStartFailed.title": "Échec du démarrage d'OpenCode",
|
|
||||||
"desktop.error.serverStartFailed.description":
|
|
||||||
"Impossible de démarrer le serveur OpenCode local. Redémarrez l'application ou vérifiez vos paramètres réseau (VPN/proxy) et réessayez.",
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import * as i18n from "@solid-primitives/i18n"
|
|
||||||
import { Store } from "@tauri-apps/plugin-store"
|
|
||||||
|
|
||||||
import { dict as desktopEn } from "./en"
|
|
||||||
import { dict as desktopZh } from "./zh"
|
|
||||||
import { dict as desktopZht } from "./zht"
|
|
||||||
import { dict as desktopKo } from "./ko"
|
|
||||||
import { dict as desktopDe } from "./de"
|
|
||||||
import { dict as desktopEs } from "./es"
|
|
||||||
import { dict as desktopFr } from "./fr"
|
|
||||||
import { dict as desktopDa } from "./da"
|
|
||||||
import { dict as desktopJa } from "./ja"
|
|
||||||
import { dict as desktopPl } from "./pl"
|
|
||||||
import { dict as desktopRu } from "./ru"
|
|
||||||
import { dict as desktopAr } from "./ar"
|
|
||||||
import { dict as desktopNo } from "./no"
|
|
||||||
import { dict as desktopBr } from "./br"
|
|
||||||
|
|
||||||
import { dict as appEn } from "../../../app/src/i18n/en"
|
|
||||||
import { dict as appZh } from "../../../app/src/i18n/zh"
|
|
||||||
import { dict as appZht } from "../../../app/src/i18n/zht"
|
|
||||||
import { dict as appKo } from "../../../app/src/i18n/ko"
|
|
||||||
import { dict as appDe } from "../../../app/src/i18n/de"
|
|
||||||
import { dict as appEs } from "../../../app/src/i18n/es"
|
|
||||||
import { dict as appFr } from "../../../app/src/i18n/fr"
|
|
||||||
import { dict as appDa } from "../../../app/src/i18n/da"
|
|
||||||
import { dict as appJa } from "../../../app/src/i18n/ja"
|
|
||||||
import { dict as appPl } from "../../../app/src/i18n/pl"
|
|
||||||
import { dict as appRu } from "../../../app/src/i18n/ru"
|
|
||||||
import { dict as appAr } from "../../../app/src/i18n/ar"
|
|
||||||
import { dict as appNo } from "../../../app/src/i18n/no"
|
|
||||||
import { dict as appBr } from "../../../app/src/i18n/br"
|
|
||||||
|
|
||||||
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
|
|
||||||
|
|
||||||
type RawDictionary = typeof appEn & typeof desktopEn
|
|
||||||
type Dictionary = i18n.Flatten<RawDictionary>
|
|
||||||
|
|
||||||
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
|
|
||||||
|
|
||||||
function detectLocale(): Locale {
|
|
||||||
if (typeof navigator !== "object") return "en"
|
|
||||||
|
|
||||||
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
|
|
||||||
for (const language of languages) {
|
|
||||||
if (!language) continue
|
|
||||||
if (language.toLowerCase().startsWith("zh")) {
|
|
||||||
if (language.toLowerCase().includes("hant")) return "zht"
|
|
||||||
return "zh"
|
|
||||||
}
|
|
||||||
if (language.toLowerCase().startsWith("ko")) return "ko"
|
|
||||||
if (language.toLowerCase().startsWith("de")) return "de"
|
|
||||||
if (language.toLowerCase().startsWith("es")) return "es"
|
|
||||||
if (language.toLowerCase().startsWith("fr")) return "fr"
|
|
||||||
if (language.toLowerCase().startsWith("da")) return "da"
|
|
||||||
if (language.toLowerCase().startsWith("ja")) return "ja"
|
|
||||||
if (language.toLowerCase().startsWith("pl")) return "pl"
|
|
||||||
if (language.toLowerCase().startsWith("ru")) return "ru"
|
|
||||||
if (language.toLowerCase().startsWith("ar")) return "ar"
|
|
||||||
if (
|
|
||||||
language.toLowerCase().startsWith("no") ||
|
|
||||||
language.toLowerCase().startsWith("nb") ||
|
|
||||||
language.toLowerCase().startsWith("nn")
|
|
||||||
)
|
|
||||||
return "no"
|
|
||||||
if (language.toLowerCase().startsWith("pt")) return "br"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "en"
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseLocale(value: unknown): Locale | null {
|
|
||||||
if (!value) return null
|
|
||||||
if (typeof value !== "string") return null
|
|
||||||
if ((LOCALES as readonly string[]).includes(value)) return value as Locale
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseRecord(value: unknown) {
|
|
||||||
if (!value || typeof value !== "object") return null
|
|
||||||
if (Array.isArray(value)) return null
|
|
||||||
return value as Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickLocale(value: unknown): Locale | null {
|
|
||||||
const direct = parseLocale(value)
|
|
||||||
if (direct) return direct
|
|
||||||
|
|
||||||
const record = parseRecord(value)
|
|
||||||
if (!record) return null
|
|
||||||
|
|
||||||
return parseLocale(record.locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = i18n.flatten({ ...appEn, ...desktopEn })
|
|
||||||
|
|
||||||
function build(locale: Locale): Dictionary {
|
|
||||||
if (locale === "en") return base
|
|
||||||
if (locale === "zh") return { ...base, ...i18n.flatten(appZh), ...i18n.flatten(desktopZh) }
|
|
||||||
if (locale === "zht") return { ...base, ...i18n.flatten(appZht), ...i18n.flatten(desktopZht) }
|
|
||||||
if (locale === "de") return { ...base, ...i18n.flatten(appDe), ...i18n.flatten(desktopDe) }
|
|
||||||
if (locale === "es") return { ...base, ...i18n.flatten(appEs), ...i18n.flatten(desktopEs) }
|
|
||||||
if (locale === "fr") return { ...base, ...i18n.flatten(appFr), ...i18n.flatten(desktopFr) }
|
|
||||||
if (locale === "da") return { ...base, ...i18n.flatten(appDa), ...i18n.flatten(desktopDa) }
|
|
||||||
if (locale === "ja") return { ...base, ...i18n.flatten(appJa), ...i18n.flatten(desktopJa) }
|
|
||||||
if (locale === "pl") return { ...base, ...i18n.flatten(appPl), ...i18n.flatten(desktopPl) }
|
|
||||||
if (locale === "ru") return { ...base, ...i18n.flatten(appRu), ...i18n.flatten(desktopRu) }
|
|
||||||
if (locale === "ar") return { ...base, ...i18n.flatten(appAr), ...i18n.flatten(desktopAr) }
|
|
||||||
if (locale === "no") return { ...base, ...i18n.flatten(appNo), ...i18n.flatten(desktopNo) }
|
|
||||||
if (locale === "br") return { ...base, ...i18n.flatten(appBr), ...i18n.flatten(desktopBr) }
|
|
||||||
return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
locale: detectLocale(),
|
|
||||||
dict: base as Dictionary,
|
|
||||||
init: undefined as Promise<Locale> | undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
state.dict = build(state.locale)
|
|
||||||
|
|
||||||
const translate = i18n.translator(() => state.dict, i18n.resolveTemplate)
|
|
||||||
|
|
||||||
export function t(key: keyof Dictionary, params?: Record<string, string | number>) {
|
|
||||||
return translate(key, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initI18n(): Promise<Locale> {
|
|
||||||
const cached = state.init
|
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const promise = (async () => {
|
|
||||||
const store = await Store.load("opencode.global.dat").catch(() => null)
|
|
||||||
if (!store) return state.locale
|
|
||||||
|
|
||||||
const raw = await store.get("language").catch(() => null)
|
|
||||||
const value = typeof raw === "string" ? JSON.parse(raw) : raw
|
|
||||||
const next = pickLocale(value) ?? state.locale
|
|
||||||
|
|
||||||
state.locale = next
|
|
||||||
state.dict = build(next)
|
|
||||||
return next
|
|
||||||
})().catch(() => state.locale)
|
|
||||||
|
|
||||||
state.init = promise
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"desktop.menu.checkForUpdates": "アップデートを確認...",
|
|
||||||
"desktop.menu.installCli": "CLI をインストール...",
|
|
||||||
"desktop.menu.reloadWebview": "Webview を再読み込み",
|
|
||||||
"desktop.menu.restart": "再起動",
|
|
||||||
|
|
||||||
"desktop.dialog.chooseFolder": "フォルダーを選択",
|
|
||||||
"desktop.dialog.chooseFile": "ファイルを選択",
|
|
||||||
"desktop.dialog.saveFile": "ファイルを保存",
|
|
||||||
|
|
||||||
"desktop.updater.checkFailed.title": "アップデートの確認に失敗しました",
|
|
||||||
"desktop.updater.checkFailed.message": "アップデートを確認できませんでした",
|
|
||||||
"desktop.updater.none.title": "利用可能なアップデートはありません",
|
|
||||||
"desktop.updater.none.message": "すでに最新バージョンの OpenCode を使用しています",
|
|
||||||
"desktop.updater.downloadFailed.title": "アップデートに失敗しました",
|
|
||||||
"desktop.updater.downloadFailed.message": "アップデートをダウンロードできませんでした",
|
|
||||||
"desktop.updater.downloaded.title": "アップデートをダウンロードしました",
|
|
||||||
"desktop.updater.downloaded.prompt":
|
|
||||||
"OpenCode のバージョン {{version}} がダウンロードされました。インストールして再起動しますか?",
|
|
||||||
"desktop.updater.installFailed.title": "アップデートに失敗しました",
|
|
||||||
"desktop.updater.installFailed.message": "アップデートをインストールできませんでした",
|
|
||||||
|
|
||||||
"desktop.cli.installed.title": "CLI をインストールしました",
|
|
||||||
"desktop.cli.installed.message":
|
|
||||||
"CLI を {{path}} にインストールしました\n\nターミナルを再起動して 'opencode' コマンドを使用してください。",
|
|
||||||
"desktop.cli.failed.title": "インストールに失敗しました",
|
|
||||||
"desktop.cli.failed.message": "CLI のインストールに失敗しました: {{error}}",
|
|
||||||
|
|
||||||
"desktop.error.serverStartFailed.title": "OpenCode の起動に失敗しました",
|
|
||||||
"desktop.error.serverStartFailed.description":
|
|
||||||
"ローカルの OpenCode サーバーを起動できませんでした。アプリを再起動するか、ネットワーク設定 (VPN/proxy) を確認して再試行してください。",
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export const dict = {
|
|
||||||
"desktop.menu.checkForUpdates": "업데이트 확인...",
|
|
||||||
"desktop.menu.installCli": "CLI 설치...",
|
|
||||||
"desktop.menu.reloadWebview": "Webview 새로고침",
|
|
||||||
"desktop.menu.restart": "다시 시작",
|
|
||||||
|
|
||||||
"desktop.dialog.chooseFolder": "폴더 선택",
|
|
||||||
"desktop.dialog.chooseFile": "파일 선택",
|
|
||||||
"desktop.dialog.saveFile": "파일 저장",
|
|
||||||
|
|
||||||
"desktop.updater.checkFailed.title": "업데이트 확인 실패",
|
|
||||||
"desktop.updater.checkFailed.message": "업데이트를 확인하지 못했습니다",
|
|
||||||
"desktop.updater.none.title": "사용 가능한 업데이트 없음",
|
|
||||||
"desktop.updater.none.message": "이미 최신 버전의 OpenCode를 사용하고 있습니다",
|
|
||||||
"desktop.updater.downloadFailed.title": "업데이트 실패",
|
|
||||||
"desktop.updater.downloadFailed.message": "업데이트를 다운로드하지 못했습니다",
|
|
||||||
"desktop.updater.downloaded.title": "업데이트 다운로드 완료",
|
|
||||||
"desktop.updater.downloaded.prompt": "OpenCode {{version}} 버전을 다운로드했습니다. 설치하고 다시 실행할까요?",
|
|
||||||
"desktop.updater.installFailed.title": "업데이트 실패",
|
|
||||||
"desktop.updater.installFailed.message": "업데이트를 설치하지 못했습니다",
|
|
||||||
|
|
||||||
"desktop.cli.installed.title": "CLI 설치됨",
|
|
||||||
"desktop.cli.installed.message":
|
|
||||||
"CLI가 {{path}}에 설치되었습니다\n\n터미널을 다시 시작하여 'opencode' 명령을 사용하세요.",
|
|
||||||
"desktop.cli.failed.title": "설치 실패",
|
|
||||||
"desktop.cli.failed.message": "CLI 설치 실패: {{error}}",
|
|
||||||
|
|
||||||
"desktop.error.serverStartFailed.title": "OpenCode 시작 실패",
|
|
||||||
"desktop.error.serverStartFailed.description":
|
|
||||||
"로컬 OpenCode 서버를 시작할 수 없습니다. 앱을 다시 시작하거나 네트워크 설정(VPN/proxy)을 확인한 후 다시 시도하세요.",
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user