mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-13 12:24:29 +00:00
Compare commits
301 Commits
release-no
...
ci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51f22c468d | ||
|
|
6b0336d475 | ||
|
|
5d5977cfef | ||
|
|
862597710f | ||
|
|
d48913b656 | ||
|
|
409a670177 | ||
|
|
213070da0e | ||
|
|
5a1412cf0b | ||
|
|
f730ad508f | ||
|
|
e3d22489aa | ||
|
|
3bc291386e | ||
|
|
d479254a57 | ||
|
|
84bcb06698 | ||
|
|
a430f0c689 | ||
|
|
b74b444b45 | ||
|
|
fe6abe8ade | ||
|
|
76c002faf4 | ||
|
|
a2a828f423 | ||
|
|
7a4f63b22a | ||
|
|
88ed3f5605 | ||
|
|
6cf533b748 | ||
|
|
b5c9e7d6ce | ||
|
|
8bc3d01c75 | ||
|
|
6f0b8c19dd | ||
|
|
a3f27dd924 | ||
|
|
0cf04a0295 | ||
|
|
a5fbb5dbec | ||
|
|
dfbf5d38f9 | ||
|
|
cbcbbe04ef | ||
|
|
8e3e208917 | ||
|
|
be2b368507 | ||
|
|
128e812500 | ||
|
|
1a3f4723e1 | ||
|
|
fd8177dad9 | ||
|
|
568a235445 | ||
|
|
9166c6e0f5 | ||
|
|
e84a8d9b19 | ||
|
|
564d373dd2 | ||
|
|
c62149129f | ||
|
|
e56016fd21 | ||
|
|
221e3182ca | ||
|
|
06399f21b4 | ||
|
|
96212f15fc | ||
|
|
26142b0deb | ||
|
|
1d972d6bae | ||
|
|
aa92ef37fd | ||
|
|
d5c59a66c1 | ||
|
|
301895c7f7 | ||
|
|
03ba49af4e | ||
|
|
008ad54cb5 | ||
|
|
571751c313 | ||
|
|
82717f6e8b | ||
|
|
c946f5f7eb | ||
|
|
2af326606c | ||
|
|
7c0067d59d | ||
|
|
92eb982863 | ||
|
|
33c5c100ff | ||
|
|
0fabdccf11 | ||
|
|
41ea4694db | ||
|
|
e84d92da28 | ||
|
|
58ba486375 | ||
|
|
121016af81 | ||
|
|
f40bdd1ac3 | ||
|
|
efc9303b27 | ||
|
|
b938e1ea99 | ||
|
|
9cfdbbb1af | ||
|
|
29ea9fcf25 | ||
|
|
870c38a6aa | ||
|
|
b937fe9450 | ||
|
|
427ef95f7d | ||
|
|
d8fe12aafc | ||
|
|
a7d7f5bb07 | ||
|
|
8cdb82038a | ||
|
|
a9e757b5bb | ||
|
|
4d2696e027 | ||
|
|
0c8de47f7d | ||
|
|
7ad165fbdc | ||
|
|
e5b33f8a5e | ||
|
|
90a7e3d64e | ||
|
|
33dc70b754 | ||
|
|
832bbba616 | ||
|
|
40d5d14304 | ||
|
|
36df0d823a | ||
|
|
9424f829eb | ||
|
|
7b561be159 | ||
|
|
c60464de07 | ||
|
|
4e41ca74b9 | ||
|
|
8c05eb22b1 | ||
|
|
f607353be6 | ||
|
|
af3c97f192 | ||
|
|
26e14ce628 | ||
|
|
57ad1814e3 | ||
|
|
4f60ea6108 | ||
|
|
775d288027 | ||
|
|
bc4968abbb | ||
|
|
acb92fcd34 | ||
|
|
c9bbea4266 | ||
|
|
65e1186efe | ||
|
|
8faa2ffcf8 | ||
|
|
bdfd8f8b0f | ||
|
|
2f35c40bb5 | ||
|
|
6650aa6f35 | ||
|
|
8798a77a72 | ||
|
|
6eb2bdd665 | ||
|
|
f97197bdb0 | ||
|
|
5a0b3ee673 | ||
|
|
3bb10773e6 | ||
|
|
558590712d | ||
|
|
d76e1448f0 | ||
|
|
026b3cc88d | ||
|
|
28e5557bf4 | ||
|
|
9a8da20a97 | ||
|
|
63f5669eb5 | ||
|
|
427cc3e153 | ||
|
|
aedd760141 | ||
|
|
6da9fb8fb9 | ||
|
|
b73e0240a5 | ||
|
|
5f2a7c630b | ||
|
|
7988f52231 | ||
|
|
e3be4c9f23 | ||
|
|
d9741866c5 | ||
|
|
898118bafb | ||
|
|
b4a9e1b190 | ||
|
|
15ffd3cba1 | ||
|
|
df7f9ae3f4 | ||
|
|
d17ba84ee1 | ||
|
|
5c8580a187 | ||
|
|
13b2587e96 | ||
|
|
605e533558 | ||
|
|
33d400c567 | ||
|
|
b8e726521d | ||
|
|
95632d893b | ||
|
|
1d5ee3e587 | ||
|
|
e5b18674f9 | ||
|
|
51edf68606 | ||
|
|
acf0df1e98 | ||
|
|
842f17d6d9 | ||
|
|
1ebf63c70c | ||
|
|
d7948c2376 | ||
|
|
892113ab39 | ||
|
|
f2bf620206 | ||
|
|
00c7729658 | ||
|
|
18d6c2191c | ||
|
|
d15201d11d | ||
|
|
1fffbc6fb3 | ||
|
|
2ca69ac953 | ||
|
|
8ee5376f9b | ||
|
|
82068955f7 | ||
|
|
df8b23db9e | ||
|
|
2649dcae7f | ||
|
|
bb63d16fa8 | ||
|
|
32ce0f4b0d | ||
|
|
6284565de2 | ||
|
|
7de42ca242 | ||
|
|
e2c57735b4 | ||
|
|
07d84fe008 | ||
|
|
b9edd23608 | ||
|
|
06e3c4a4f2 | ||
|
|
2f5a238b51 | ||
|
|
173faca3c6 | ||
|
|
8c00818108 | ||
|
|
f12f7e7718 | ||
|
|
0aa93379bd | ||
|
|
dbc8d7edca | ||
|
|
d8e7e915e2 | ||
|
|
712d2b7d15 | ||
|
|
00e79210e5 | ||
|
|
099ab929db | ||
|
|
eac2d4c699 | ||
|
|
3297e5230e | ||
|
|
c3d8d2b27f | ||
|
|
19c7874493 | ||
|
|
27bb82761b | ||
|
|
c7e2f1965d | ||
|
|
3e420bf8e1 | ||
|
|
ad624f65ee | ||
|
|
c68261fc06 | ||
|
|
2d0049f0d9 | ||
|
|
52387e7f33 | ||
|
|
ebfa2b57da | ||
|
|
03b9317f49 | ||
|
|
3862b1aeda | ||
|
|
b4e0cdbe8e | ||
|
|
095328faf4 | ||
|
|
743e83d9bf | ||
|
|
1f9313847f | ||
|
|
2180be2f3f | ||
|
|
58b9b54600 | ||
|
|
c0a5f85349 | ||
|
|
b6565c606e | ||
|
|
ddffb34b99 | ||
|
|
dd1624e30e | ||
|
|
213c0e18ab | ||
|
|
a8c18dba82 | ||
|
|
6cf2c3e3db | ||
|
|
b59aec6f04 | ||
|
|
d9e8b2b65d | ||
|
|
7655f51e10 | ||
|
|
e7c6267323 | ||
|
|
64a3661a32 | ||
|
|
bf463aee04 | ||
|
|
b24fd90fe8 | ||
|
|
6897bb7d02 | ||
|
|
8371ba5aec | ||
|
|
36577479c5 | ||
|
|
bb178e9352 | ||
|
|
4075f9e1ab | ||
|
|
021d9d105e | ||
|
|
b07d7cdb71 | ||
|
|
77f11dfabe | ||
|
|
45b09c1465 | ||
|
|
ccc7aa49c3 | ||
|
|
53ac394c68 | ||
|
|
8b6484ac1a | ||
|
|
c1e840b9b2 | ||
|
|
a77df3c174 | ||
|
|
9d1cf98192 | ||
|
|
7b3d5f1d68 | ||
|
|
b21f82f5b0 | ||
|
|
d82e94c209 | ||
|
|
cbe8f265b9 | ||
|
|
8b5dde5536 | ||
|
|
6a62b44593 | ||
|
|
c700b928e4 | ||
|
|
ae815cca3a | ||
|
|
5a16d99b60 | ||
|
|
3f9b59c798 | ||
|
|
c6febd8ddd | ||
|
|
010ed55590 | ||
|
|
2be4598011 | ||
|
|
99cd7f3468 | ||
|
|
fca0825b71 | ||
|
|
9babdb80cf | ||
|
|
f4392e023a | ||
|
|
0e08c6c9fd | ||
|
|
0dcb850a7a | ||
|
|
d9a61cd94c | ||
|
|
0f4a10f4a3 | ||
|
|
c551f7e47b | ||
|
|
7962ff38b0 | ||
|
|
3ac11df66a | ||
|
|
7caf59b433 | ||
|
|
92229b44f8 | ||
|
|
0a572afd46 | ||
|
|
ecd04a118a | ||
|
|
6f3d413472 | ||
|
|
7c96d704d3 | ||
|
|
9346c1ae3f | ||
|
|
e0e97e9d93 | ||
|
|
810bc012b6 | ||
|
|
36b832880d | ||
|
|
d4e3acf17e | ||
|
|
04337f6202 | ||
|
|
97aec21cb3 | ||
|
|
319ad2a391 | ||
|
|
de3b654dcd | ||
|
|
8b17ac656c | ||
|
|
18bfc740c8 | ||
|
|
837037cd04 | ||
|
|
b0f865eae5 | ||
|
|
ac53a372b0 | ||
|
|
7795cae0b5 | ||
|
|
4c9d879624 | ||
|
|
3fdd08d66e | ||
|
|
ec2ab639bb | ||
|
|
d05ed5ca83 | ||
|
|
37f1a1a4ef | ||
|
|
b8e8d82323 | ||
|
|
801eb5d2cb | ||
|
|
ebeed03115 | ||
|
|
d9eed4c6ca | ||
|
|
7e34d27b77 | ||
|
|
783121c06e | ||
|
|
984518b1c0 | ||
|
|
7fcdbd155b | ||
|
|
5856ea4e75 | ||
|
|
32a0dcedcb | ||
|
|
f48784d152 | ||
|
|
3dce6a6608 | ||
|
|
39a73d4894 | ||
|
|
b1fbfa7e94 | ||
|
|
805ae19c9a | ||
|
|
fcea7e18a5 | ||
|
|
7c34319b19 | ||
|
|
cd4676171b | ||
|
|
7016be0739 | ||
|
|
ff35db0360 | ||
|
|
af3d8c383e | ||
|
|
7f75f71f6b | ||
|
|
84b12a8fb7 | ||
|
|
1934ee13d8 | ||
|
|
6c1e18f111 | ||
|
|
3296b90372 | ||
|
|
0d651eab3b | ||
|
|
0edd304f42 | ||
|
|
6b83b172ae | ||
|
|
444934a4c1 | ||
|
|
c4f1087e58 | ||
|
|
c87232d5df | ||
|
|
d03c5f6b3f | ||
|
|
9a33b1ec88 |
83
.github/workflows/close-stale-prs.yml
vendored
Normal file
83
.github/workflows/close-stale-prs.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Close stale PRs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dryRun:
|
||||
description: "Log actions without closing PRs"
|
||||
type: boolean
|
||||
default: false
|
||||
schedule:
|
||||
- cron: "0 6 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
close-stale-prs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close inactive PRs
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const DAYS_INACTIVE = 60
|
||||
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
||||
const { owner, repo } = context.repo
|
||||
const dryRun = context.payload.inputs?.dryRun === "true"
|
||||
const stalePrs = []
|
||||
|
||||
core.info(`Dry run mode: ${dryRun}`)
|
||||
|
||||
const prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
per_page: 100,
|
||||
sort: "updated",
|
||||
direction: "asc",
|
||||
})
|
||||
|
||||
for (const pr of prs) {
|
||||
const lastUpdated = new Date(pr.updated_at)
|
||||
if (lastUpdated > cutoff) {
|
||||
core.info(`PR ${pr.number} is fresh`)
|
||||
continue
|
||||
}
|
||||
|
||||
stalePrs.push(pr)
|
||||
}
|
||||
|
||||
if (!stalePrs.length) {
|
||||
core.info("No stale pull requests found.")
|
||||
return
|
||||
}
|
||||
|
||||
for (const pr of stalePrs) {
|
||||
const issue_number = pr.number
|
||||
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.user.login}`)
|
||||
continue
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body: closeComment,
|
||||
})
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue_number,
|
||||
state: "closed",
|
||||
})
|
||||
|
||||
core.info(`Closed PR #${issue_number} from ${pr.user.login}`)
|
||||
}
|
||||
33
.github/workflows/contributors-label.yml
vendored
Normal file
33
.github/workflows/contributors-label.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
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']
|
||||
});
|
||||
}
|
||||
163
.github/workflows/publish.yml
vendored
163
.github/workflows/publish.yml
vendored
@@ -4,6 +4,7 @@ run-name: "${{ format('release {0}', inputs.bump) }}"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- ci
|
||||
- dev
|
||||
- snapshot-*
|
||||
workflow_dispatch:
|
||||
@@ -29,56 +30,46 @@ permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
version:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
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 }}
|
||||
|
||||
- run: git fetch --force --tags
|
||||
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: Install OpenCode
|
||||
if: inputs.bump || inputs.version
|
||||
run: bun i -g opencode-ai@1.0.169
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Setup Git Identity
|
||||
- name: Build
|
||||
id: build
|
||||
run: |
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
||||
|
||||
- name: Publish
|
||||
id: publish
|
||||
run: ./script/publish-start.ts
|
||||
./packages/opencode/script/build.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
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -86,12 +77,12 @@ jobs:
|
||||
path: packages/opencode/dist
|
||||
|
||||
outputs:
|
||||
release: ${{ steps.publish.outputs.release }}
|
||||
tag: ${{ steps.publish.outputs.tag }}
|
||||
version: ${{ steps.publish.outputs.version }}
|
||||
version: ${{ needs.version.outputs.version }}
|
||||
|
||||
publish-tauri:
|
||||
needs: publish
|
||||
build-tauri:
|
||||
needs:
|
||||
- build-cli
|
||||
- version
|
||||
continue-on-error: false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -111,8 +102,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.publish.outputs.tag }}
|
||||
fetch-depth: 1
|
||||
fetch-tags: true
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
@@ -134,8 +125,6 @@ jobs:
|
||||
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)
|
||||
@@ -160,10 +149,7 @@ jobs:
|
||||
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 }}
|
||||
@@ -177,22 +163,18 @@ jobs:
|
||||
cargo tauri --version
|
||||
|
||||
- name: Build and upload artifacts
|
||||
uses: Wandalen/wretry.action@v3
|
||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
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
|
||||
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
|
||||
@@ -205,20 +187,52 @@ jobs:
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||
|
||||
publish-release:
|
||||
publish:
|
||||
needs:
|
||||
- publish
|
||||
- publish-tauri
|
||||
if: needs.publish.outputs.tag
|
||||
- version
|
||||
- build-cli
|
||||
- build-tauri
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.publish.outputs.tag }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install OpenCode
|
||||
if: inputs.bump || inputs.version
|
||||
run: bun i -g opencode-ai
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Setup Git Identity
|
||||
run: |
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: opencode-cli
|
||||
path: packages/opencode/dist
|
||||
|
||||
- name: Setup SSH for AUR
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -230,8 +244,11 @@ jobs:
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
|
||||
- run: ./script/publish-complete.ts
|
||||
- run: ./script/publish.ts
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
NPM_CONFIG_PROVENANCE: false
|
||||
|
||||
@@ -7,7 +7,7 @@ Please read @package.json and @packages/opencode/package.json.
|
||||
Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes).
|
||||
|
||||
I want a report of every dependency and the version that can be upgraded to.
|
||||
What would be even better is if you can give me links to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
|
||||
What would be even better is if you can give me brief summary of the changes for each dep and a link to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
|
||||
|
||||
Consider using subagents for each dep to save your context window.
|
||||
|
||||
|
||||
42
.opencode/command/learn.md
Normal file
42
.opencode/command/learn.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
description: Extract non-obvious learnings from session to AGENTS.md files to build codebase understanding
|
||||
---
|
||||
|
||||
Analyze this session and extract non-obvious learnings to add to AGENTS.md files.
|
||||
|
||||
AGENTS.md files can exist at any directory level, not just the project root. When an agent reads a file, any AGENTS.md in parent directories are automatically loaded into the context of the tool read. Place learnings as close to the relevant code as possible:
|
||||
|
||||
- Project-wide learnings → root AGENTS.md
|
||||
- Package/module-specific → packages/foo/AGENTS.md
|
||||
- Feature-specific → src/auth/AGENTS.md
|
||||
|
||||
What counts as a learning (non-obvious discoveries only):
|
||||
|
||||
- Hidden relationships between files or modules
|
||||
- Execution paths that differ from how code appears
|
||||
- Non-obvious configuration, env vars, or flags
|
||||
- Debugging breakthroughs when error messages were misleading
|
||||
- API/tool quirks and workarounds
|
||||
- Build/test commands not in README
|
||||
- Architectural decisions and constraints
|
||||
- Files that must change together
|
||||
|
||||
What NOT to include:
|
||||
|
||||
- Obvious facts from documentation
|
||||
- Standard language/framework behavior
|
||||
- Things already in an AGENTS.md
|
||||
- Verbose explanations
|
||||
- Session-specific details
|
||||
|
||||
Process:
|
||||
|
||||
1. Review session for discoveries, errors that took multiple attempts, unexpected connections
|
||||
2. Determine scope - what directory does each learning apply to?
|
||||
3. Read existing AGENTS.md files at relevant levels
|
||||
4. Create or update AGENTS.md at the appropriate level
|
||||
5. Keep entries to 1-3 lines per insight
|
||||
|
||||
After updating, summarize which AGENTS.md files were created/updated and how many learnings per file.
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
133
README.it.md
Normal file
133
README.it.md
Normal file
@@ -0,0 +1,133 @@
|
||||
<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="Logo OpenCode">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">L’agente di coding AI open source.</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="Build status" 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>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
---
|
||||
|
||||
### Installazione
|
||||
|
||||
```bash
|
||||
# YOLO
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# Package manager
|
||||
npm i -g opencode-ai@latest # oppure bun/pnpm/yarn
|
||||
scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS e Linux (consigliato, sempre aggiornato)
|
||||
brew install opencode # macOS e Linux (formula brew ufficiale, aggiornata meno spesso)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # Qualsiasi OS
|
||||
nix run nixpkgs#opencode # oppure github:anomalyco/opencode per l’ultima branch di sviluppo
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Rimuovi le versioni precedenti alla 0.1.x prima di installare.
|
||||
|
||||
### App Desktop (BETA)
|
||||
|
||||
OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla direttamente dalla [pagina delle release](https://github.com/anomalyco/opencode/releases) oppure da [opencode.ai/download](https://opencode.ai/download).
|
||||
|
||||
| Piattaforma | 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`, oppure AppImage |
|
||||
|
||||
```bash
|
||||
# macOS (Homebrew)
|
||||
brew install --cask opencode-desktop
|
||||
# Windows (Scoop)
|
||||
scoop bucket add extras; scoop install extras/opencode-desktop
|
||||
```
|
||||
|
||||
#### Directory di installazione
|
||||
|
||||
Lo script di installazione rispetta il seguente ordine di priorità per il percorso di installazione:
|
||||
|
||||
1. `$OPENCODE_INSTALL_DIR` – Directory di installazione personalizzata
|
||||
2. `$XDG_BIN_DIR` – Percorso conforme alla XDG Base Directory Specification
|
||||
3. `$HOME/bin` – Directory binaria standard dell’utente (se esiste o può essere creata)
|
||||
4. `$HOME/.opencode/bin` – Fallback predefinito
|
||||
|
||||
```bash
|
||||
# Esempi
|
||||
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
|
||||
```
|
||||
|
||||
### Agenti
|
||||
|
||||
OpenCode include due agenti integrati tra cui puoi passare usando il tasto `Tab`.
|
||||
|
||||
- **build** – Predefinito, agente con accesso completo per il lavoro di sviluppo
|
||||
- **plan** – Agente in sola lettura per analisi ed esplorazione del codice
|
||||
- Nega le modifiche ai file per impostazione predefinita
|
||||
- Chiede il permesso prima di eseguire comandi bash
|
||||
- Ideale per esplorare codebase sconosciute o pianificare modifiche
|
||||
|
||||
È inoltre incluso un sotto-agente **general** per ricerche complesse e attività multi-step.
|
||||
Viene utilizzato internamente e può essere invocato usando `@general` nei messaggi.
|
||||
|
||||
Scopri di più sugli [agenti](https://opencode.ai/docs/agents).
|
||||
|
||||
### Documentazione
|
||||
|
||||
Per maggiori informazioni su come configurare OpenCode, [**consulta la nostra documentazione**](https://opencode.ai/docs).
|
||||
|
||||
### Contribuire
|
||||
|
||||
Se sei interessato a contribuire a OpenCode, leggi la nostra [guida alla contribuzione](./CONTRIBUTING.md) prima di inviare una pull request.
|
||||
|
||||
### Costruire su OpenCode
|
||||
|
||||
Se stai lavorando a un progetto correlato a OpenCode e che utilizza “opencode” come parte del nome (ad esempio “opencode-dashboard” o “opencode-mobile”), aggiungi una nota nel tuo README per chiarire che non è sviluppato dal team OpenCode e che non è affiliato in alcun modo con noi.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### In cosa è diverso da Claude Code?
|
||||
|
||||
È molto simile a Claude Code in termini di funzionalità. Ecco le principali differenze:
|
||||
|
||||
- 100% open source
|
||||
- Non è legato a nessun provider. Anche se consigliamo i modelli forniti tramite [OpenCode Zen](https://opencode.ai/zen), OpenCode può essere utilizzato con Claude, OpenAI, Google o persino modelli locali. Con l’evoluzione dei modelli, le differenze tra di essi si ridurranno e i prezzi scenderanno, quindi essere indipendenti dal provider è importante.
|
||||
- Supporto LSP pronto all’uso
|
||||
- Forte attenzione alla TUI. OpenCode è sviluppato da utenti neovim e dai creatori di [terminal.shop](https://terminal.shop); spingeremo al limite ciò che è possibile fare nel terminale.
|
||||
- Architettura client/server. Questo, ad esempio, permette a OpenCode di girare sul tuo computer mentre lo controlli da remoto tramite un’app mobile. La frontend TUI è quindi solo uno dei possibili client.
|
||||
|
||||
---
|
||||
|
||||
**Unisciti alla nostra community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,13 +22,15 @@
|
||||
<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.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
134
README.th.md
Normal file
134
README.th.md
Normal file
@@ -0,0 +1,134 @@
|
||||
<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)
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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> |
|
||||
|
||||
426
STATS.md
426
STATS.md
@@ -1,213 +1,217 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | -------------------- | -------------------- |
|
||||
| 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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 | 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | -------------------- | --------------------- |
|
||||
| 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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 | 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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) |
|
||||
|
||||
54
bun.lock
54
bun.lock
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -107,7 +107,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -182,12 +182,14 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solid-primitives/i18n": "2.2.1",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-http": "~2",
|
||||
"@tauri-apps/plugin-notification": "~2",
|
||||
@@ -211,7 +213,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -240,7 +242,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -256,7 +258,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -296,8 +298,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.74",
|
||||
"@opentui/solid": "0.1.74",
|
||||
"@opentui/core": "0.1.75",
|
||||
"@opentui/solid": "0.1.75",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -360,7 +362,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -380,7 +382,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -391,7 +393,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -404,7 +406,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -446,7 +448,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -457,7 +459,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1227,21 +1229,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.74", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.74", "@opentui/core-darwin-x64": "0.1.74", "@opentui/core-linux-arm64": "0.1.74", "@opentui/core-linux-x64": "0.1.74", "@opentui/core-win32-arm64": "0.1.74", "@opentui/core-win32-x64": "0.1.74", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g4W16ymv12JdgZ+9B4t7mpIICvzWy2+eHERfmDf80ALduOQCUedKQdULcBFhVCYUXIkDRtIy6CID5thMAah3FA=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.75", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.75", "@opentui/core-darwin-x64": "0.1.75", "@opentui/core-linux-arm64": "0.1.75", "@opentui/core-linux-x64": "0.1.75", "@opentui/core-win32-arm64": "0.1.75", "@opentui/core-win32-x64": "0.1.75", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.75", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGaGZjkFpqcXJk6321JzhRl66pM2VxBlI470L8W4DQUW4S6iDT1R9L7awSzGB4Cn9toUl7DTV8BemaXZYXV4SA=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.75", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPlvqQI0whZ76amHydpJs5kN+QeWAIcFbI8RAtlAo9baj2EbxTDC+JGwgb9Fnt0/YQx831humbtaNDhV2Jt1bw=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.75", "", { "os": "linux", "cpu": "arm64" }, "sha512-nVxIQ4Hqf84uBergDpWiVzU6pzpjy6tqBHRQpySxZ2flkJ/U6/aMEizVrQ1jcgIdxZtvqWDETZhzxhG0yDx+cw=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.74", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.75", "", { "os": "linux", "cpu": "x64" }, "sha512-1CnApef4kxA+ORyLfbuCLgZfEjp4wr3HjFnt7FAfOb73kIZH82cb7JYixeqRyy9eOcKfKqxLmBYy3o8IDkc4Rg=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.75", "", { "os": "win32", "cpu": "arm64" }, "sha512-j0UB95nmkYGNzmOrs6GqaddO1S90R0YC6IhbKnbKBdjchFPNVLz9JpexAs6MBDXPZwdKAywMxtwG2h3aTJtxng=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.74", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.75", "", { "os": "win32", "cpu": "x64" }, "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.74", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.74", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-Vz82cI8T9YeJjGsVg4ULp6ral4N+xyt1j9A6Tbu3aaQgEKiB74LW03EXREehfjPr1irOFxtKfWPbx5NKH0Upag=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1747,6 +1749,8 @@
|
||||
|
||||
"@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-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-olTZ+tKugAY3LxizsJMlbK3TW78HZUoM03PigvQLP4A=",
|
||||
"aarch64-linux": "sha256-xdKDeqMEnYM2+vGySfb8pbcYyo/xMmgxG/ZhPCKaZEg=",
|
||||
"aarch64-darwin": "sha256-fihCTrHIiUG+py4vuqdr+YshqSKm2/B5onY50b97sPM=",
|
||||
"x86_64-darwin": "sha256-inlQQPNAOdkmKK6HQAMI2bG/ZFlfwmUQu9a6vm6Q0jQ="
|
||||
"x86_64-linux": "sha256-yAtZlh6YR78RwPt0LK/7Pk0qUm0/97+s6ghhZzuoE/0=",
|
||||
"aarch64-linux": "sha256-6j81rdjQ7Wps9bvfw+mmdwW5p01qUOwX40UZltCTe3Y=",
|
||||
"aarch64-darwin": "sha256-pDM8M/QMWR6Go5pz3XXsJqcJDHAlHrx2Faijjkzcngo=",
|
||||
"x86_64-darwin": "sha256-eOAPtMd1n5xYupBOevCLhY1eFy3wzGqFk/EsZocl9Y8="
|
||||
}
|
||||
}
|
||||
|
||||
37
packages/app/e2e/file-tree.spec.ts
Normal file
37
packages/app/e2e/file-tree.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
|
||||
test("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const toggle = page.getByRole("button", { name: "Toggle file tree" })
|
||||
const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
|
||||
|
||||
if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click()
|
||||
await expect(treeTabs).toBeVisible()
|
||||
|
||||
await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click()
|
||||
|
||||
const node = (name: string) => treeTabs.getByRole("button", { name, exact: true })
|
||||
|
||||
await expect(node("packages")).toBeVisible()
|
||||
await node("packages").click()
|
||||
|
||||
await expect(node("app")).toBeVisible()
|
||||
await node("app").click()
|
||||
|
||||
await expect(node("src")).toBeVisible()
|
||||
await node("src").click()
|
||||
|
||||
await expect(node("components")).toBeVisible()
|
||||
await node("components").click()
|
||||
|
||||
await expect(node("file-tree.tsx")).toBeVisible()
|
||||
await node("file-tree.tsx").click()
|
||||
|
||||
const tab = page.getByRole("tab", { name: "file-tree.tsx" })
|
||||
await expect(tab).toBeVisible()
|
||||
await tab.click()
|
||||
|
||||
const code = page.locator('[data-component="code"]').first()
|
||||
await expect(code.getByText("export default function FileTree")).toBeVisible()
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test as base, expect } from "@playwright/test"
|
||||
import { createSdk, dirSlug, getWorktree, promptSelector, sessionPath } from "./utils"
|
||||
import { createSdk, dirSlug, getWorktree, promptSelector, serverUrl, sessionPath } from "./utils"
|
||||
|
||||
type TestFixtures = {
|
||||
sdk: ReturnType<typeof createSdk>
|
||||
@@ -29,6 +29,55 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||
await use(createSdk(directory))
|
||||
},
|
||||
gotoSession: async ({ page, directory }, use) => {
|
||||
await page.addInitScript(
|
||||
(input: { directory: string; serverUrl: string }) => {
|
||||
const key = "opencode.global.dat:server"
|
||||
const raw = localStorage.getItem(key)
|
||||
const parsed = (() => {
|
||||
if (!raw) return undefined
|
||||
try {
|
||||
return JSON.parse(raw) as unknown
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
|
||||
const store = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {}
|
||||
const list = Array.isArray(store.list) ? store.list : []
|
||||
const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {}
|
||||
const projects = store.projects && typeof store.projects === "object" ? store.projects : {}
|
||||
const nextProjects = { ...(projects as Record<string, unknown>) }
|
||||
|
||||
const add = (origin: string) => {
|
||||
const current = nextProjects[origin]
|
||||
const items = Array.isArray(current) ? current : []
|
||||
const existing = items.filter(
|
||||
(p): p is { worktree: string; expanded?: boolean } =>
|
||||
!!p &&
|
||||
typeof p === "object" &&
|
||||
"worktree" in p &&
|
||||
typeof (p as { worktree?: unknown }).worktree === "string",
|
||||
)
|
||||
|
||||
if (existing.some((p) => p.worktree === input.directory)) return
|
||||
nextProjects[origin] = [{ worktree: input.directory, expanded: true }, ...existing]
|
||||
}
|
||||
|
||||
add("local")
|
||||
add(input.serverUrl)
|
||||
|
||||
localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
list,
|
||||
projects: nextProjects,
|
||||
lastProject,
|
||||
}),
|
||||
)
|
||||
},
|
||||
{ directory, serverUrl },
|
||||
)
|
||||
|
||||
const gotoSession = async (sessionID?: string) => {
|
||||
await page.goto(sessionPath(directory, sessionID))
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
|
||||
86
packages/app/e2e/models-visibility.spec.ts
Normal file
86
packages/app/e2e/models-visibility.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
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)
|
||||
})
|
||||
67
packages/app/e2e/server-default.spec.ts
Normal file
67
packages/app/e2e/server-default.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
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()
|
||||
})
|
||||
56
packages/app/e2e/settings-providers.spec.ts
Normal file
56
packages/app/e2e/settings-providers.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
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)
|
||||
})
|
||||
61
packages/app/e2e/sidebar-session-links.spec.ts
Normal file
61
packages/app/e2e/sidebar-session-links.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { modKey, promptSelector } from "./utils"
|
||||
|
||||
type Locator = {
|
||||
first: () => Locator
|
||||
getAttribute: (name: string) => Promise<string | null>
|
||||
scrollIntoViewIfNeeded: () => Promise<void>
|
||||
click: () => Promise<void>
|
||||
}
|
||||
|
||||
type Page = {
|
||||
locator: (selector: string) => Locator
|
||||
keyboard: {
|
||||
press: (key: string) => Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
type Fixtures = {
|
||||
page: Page
|
||||
slug: string
|
||||
sdk: {
|
||||
session: {
|
||||
create: (input: { title: string }) => Promise<{ data?: { id?: string } }>
|
||||
delete: (input: { sessionID: string }) => Promise<unknown>
|
||||
}
|
||||
}
|
||||
gotoSession: (sessionID?: string) => Promise<void>
|
||||
}
|
||||
|
||||
test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }: Fixtures) => {
|
||||
const stamp = Date.now()
|
||||
|
||||
const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data)
|
||||
const two = await sdk.session.create({ title: `e2e sidebar nav 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 target = page.locator(`[data-session-id="${two.id}"] a`).first()
|
||||
await expect(target).toBeVisible()
|
||||
await target.scrollIntoViewIfNeeded()
|
||||
await target.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/)
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
|
||||
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
52
packages/app/e2e/titlebar-history.spec.ts
Normal file
52
packages/app/e2e/titlebar-history.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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",
|
||||
"version": "1.1.36",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
@@ -14,22 +14,22 @@ import { GlobalSyncProvider } from "@/context/global-sync"
|
||||
import { PermissionProvider } from "@/context/permission"
|
||||
import { LayoutProvider } from "@/context/layout"
|
||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||
import { ServerProvider, useServer } from "@/context/server"
|
||||
import { normalizeServerUrl, ServerProvider, useServer } from "@/context/server"
|
||||
import { SettingsProvider } from "@/context/settings"
|
||||
import { TerminalProvider } from "@/context/terminal"
|
||||
import { PromptProvider } from "@/context/prompt"
|
||||
import { FileProvider } from "@/context/file"
|
||||
import { CommentsProvider } from "@/context/comments"
|
||||
import { NotificationProvider } from "@/context/notification"
|
||||
import { ModelsProvider } from "@/context/models"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { CommandProvider } from "@/context/command"
|
||||
import { LanguageProvider, useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import { HighlightsProvider } from "@/context/highlights"
|
||||
import Layout from "@/pages/layout"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Suspense } from "solid-js"
|
||||
|
||||
const Home = lazy(() => import("@/pages/home"))
|
||||
@@ -43,7 +43,7 @@ function UiI18nBridge(props: ParentProps) {
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
|
||||
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,19 @@ function ServerKey(props: ParentProps) {
|
||||
}
|
||||
|
||||
export function AppInterface(props: { defaultUrl?: string }) {
|
||||
const platform = usePlatform()
|
||||
|
||||
const stored = (() => {
|
||||
if (platform.platform !== "web") return
|
||||
const result = platform.getDefaultServerUrl?.()
|
||||
if (result instanceof Promise) return
|
||||
if (!result) return
|
||||
return normalizeServerUrl(result)
|
||||
})()
|
||||
|
||||
const defaultServerUrl = () => {
|
||||
if (props.defaultUrl) return props.defaultUrl
|
||||
if (stored) return stored
|
||||
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||
if (import.meta.env.DEV)
|
||||
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
||||
@@ -105,9 +116,13 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
<ModelsProvider>
|
||||
<CommandProvider>
|
||||
<HighlightsProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</HighlightsProvider>
|
||||
</CommandProvider>
|
||||
</ModelsProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
|
||||
@@ -27,6 +27,17 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
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 methods = createMemo(
|
||||
() =>
|
||||
@@ -53,6 +64,11 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
}
|
||||
|
||||
async function selectMethod(index: number) {
|
||||
if (timer.current !== undefined) {
|
||||
clearTimeout(timer.current)
|
||||
timer.current = undefined
|
||||
}
|
||||
|
||||
const method = methods()[index]
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
@@ -75,11 +91,15 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.then((x) => {
|
||||
if (!alive.value) return
|
||||
const elapsed = Date.now() - start
|
||||
const delay = 1000 - elapsed
|
||||
|
||||
if (delay > 0) {
|
||||
setTimeout(() => {
|
||||
if (timer.current !== undefined) clearTimeout(timer.current)
|
||||
timer.current = setTimeout(() => {
|
||||
timer.current = undefined
|
||||
if (!alive.value) return
|
||||
setStore("state", "complete")
|
||||
setStore("authorization", x.data!)
|
||||
}, delay)
|
||||
@@ -89,6 +109,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
setStore("authorization", x.data!)
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!alive.value) return
|
||||
setStore("state", "error")
|
||||
setStore("error", String(e))
|
||||
})
|
||||
@@ -372,26 +393,33 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
return instructions
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
if (store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
}
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
})
|
||||
.then((value) =>
|
||||
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
|
||||
)
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
if (!result.ok) {
|
||||
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
||||
setStore("state", "error")
|
||||
setStore("error", message)
|
||||
return
|
||||
}
|
||||
await complete()
|
||||
onMount(() => {
|
||||
void (async () => {
|
||||
if (store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
}
|
||||
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
})
|
||||
.then((value) =>
|
||||
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
|
||||
)
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
|
||||
if (!alive.value) return
|
||||
|
||||
if (!result.ok) {
|
||||
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
||||
setStore("state", "error")
|
||||
setStore("error", message)
|
||||
return
|
||||
}
|
||||
|
||||
await complete()
|
||||
})()
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
424
packages/app/src/components/dialog-custom-provider.tsx
Normal file
424
packages/app/src/components/dialog-custom-provider.tsx
Normal file
@@ -0,0 +1,424 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
@@ -29,35 +29,34 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
iconUrl: props.project.icon?.override || "",
|
||||
startup: props.project.commands?.start ?? "",
|
||||
saving: false,
|
||||
dragOver: false,
|
||||
iconHover: false,
|
||||
})
|
||||
|
||||
const [dragOver, setDragOver] = createSignal(false)
|
||||
const [iconHover, setIconHover] = createSignal(false)
|
||||
|
||||
function handleFileSelect(file: File) {
|
||||
if (!file.type.startsWith("image/")) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
setStore("iconUrl", e.target?.result as string)
|
||||
setIconHover(false)
|
||||
setStore("iconHover", false)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
setStore("dragOver", false)
|
||||
const file = e.dataTransfer?.files[0]
|
||||
if (file) handleFileSelect(file)
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
setStore("dragOver", true)
|
||||
}
|
||||
|
||||
function handleDragLeave() {
|
||||
setDragOver(false)
|
||||
setStore("dragOver", false)
|
||||
}
|
||||
|
||||
function handleInputChange(e: Event) {
|
||||
@@ -116,19 +115,23 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
|
||||
<div class="flex gap-3 items-start">
|
||||
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
|
||||
<div
|
||||
class="relative"
|
||||
onMouseEnter={() => setStore("iconHover", true)}
|
||||
onMouseLeave={() => setStore("iconHover", false)}
|
||||
>
|
||||
<div
|
||||
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
||||
classList={{
|
||||
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
||||
"border-border-base hover:border-border-strong": !dragOver(),
|
||||
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
||||
"border-border-base hover:border-border-strong": !store.dragOver,
|
||||
"overflow-hidden": !!store.iconUrl,
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => {
|
||||
if (store.iconUrl && iconHover()) {
|
||||
if (store.iconUrl && store.iconHover) {
|
||||
clearIcon()
|
||||
} else {
|
||||
document.getElementById("icon-upload")?.click()
|
||||
@@ -142,8 +145,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
<Avatar
|
||||
fallback={store.name || defaultName()}
|
||||
{...getAvatarColors(store.color)}
|
||||
class="size-full"
|
||||
style={{ "font-size": "32px" }}
|
||||
class="size-full text-[32px]"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -156,39 +158,19 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
</Show>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
"border-radius": "6px",
|
||||
"z-index": 10,
|
||||
"pointer-events": "none",
|
||||
opacity: iconHover() && !store.iconUrl ? 1 : 0,
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
classList={{
|
||||
"opacity-100": store.iconHover && !store.iconUrl,
|
||||
"opacity-0": !(store.iconHover && !store.iconUrl),
|
||||
}}
|
||||
>
|
||||
<Icon name="cloud-upload" size="large" class="text-icon-invert-base" />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
"border-radius": "6px",
|
||||
"z-index": 10,
|
||||
"pointer-events": "none",
|
||||
opacity: iconHover() && store.iconUrl ? 1 : 0,
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
classList={{
|
||||
"opacity-100": store.iconHover && !!store.iconUrl,
|
||||
"opacity-0": !(store.iconHover && !!store.iconUrl),
|
||||
}}
|
||||
>
|
||||
<Icon name="trash" size="large" class="text-icon-invert-base" />
|
||||
|
||||
@@ -90,12 +90,8 @@ export const DialogFork: Component = () => {
|
||||
>
|
||||
{(item) => (
|
||||
<div class="w-full flex items-center gap-2">
|
||||
<span class="truncate flex-1 min-w-0 text-left" style={{ "font-weight": "400" }}>
|
||||
{item.text}
|
||||
</span>
|
||||
<span class="text-text-weak shrink-0" style={{ "font-weight": "400" }}>
|
||||
{item.time}
|
||||
</span>
|
||||
<span class="truncate flex-1 min-w-0 text-left font-normal">{item.text}</span>
|
||||
<span class="text-text-weak shrink-0 font-normal">{item.time}</span>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import type { Component } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
|
||||
export const DialogManageModels: Component = () => {
|
||||
const local = useLocal()
|
||||
const language = useLanguage()
|
||||
const dialog = useDialog()
|
||||
|
||||
const handleConnectProvider = () => {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={language.t("dialog.model.manage")} description={language.t("dialog.model.manage.description")}>
|
||||
<Dialog
|
||||
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
|
||||
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.model.empty")}
|
||||
|
||||
@@ -2,113 +2,11 @@ import { createSignal, createEffect, onMount, onCleanup } from "solid-js"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { markReleaseNotesSeen } from "@/lib/release-notes"
|
||||
import { useSettings } from "@/context/settings"
|
||||
|
||||
const CHANGELOG_URL = "https://opencode.ai/changelog.json"
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null
|
||||
}
|
||||
|
||||
function getText(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
const text = value.trim()
|
||||
return text.length > 0 ? text : undefined
|
||||
}
|
||||
|
||||
if (!Array.isArray(value)) return
|
||||
const parts = value.map((item) => (typeof item === "string" ? item.trim() : "")).filter((item) => item.length > 0)
|
||||
if (parts.length === 0) return
|
||||
return parts.join(" ")
|
||||
}
|
||||
|
||||
function normalizeRemoteUrl(url: string): string {
|
||||
if (url.startsWith("https://") || url.startsWith("http://")) return url
|
||||
if (url.startsWith("/")) return `https://opencode.ai${url}`
|
||||
return `https://opencode.ai/${url}`
|
||||
}
|
||||
|
||||
function parseMedia(value: unknown): ReleaseFeature["media"] | undefined {
|
||||
if (!isRecord(value)) return
|
||||
|
||||
const type = getText(value.type)?.toLowerCase()
|
||||
const src = getText(value.src)
|
||||
if (!src) return
|
||||
if (type !== "image" && type !== "video") return
|
||||
|
||||
return {
|
||||
type,
|
||||
src: normalizeRemoteUrl(src),
|
||||
alt: getText(value.alt),
|
||||
}
|
||||
}
|
||||
|
||||
function parseFeature(value: unknown): ReleaseFeature | undefined {
|
||||
if (!isRecord(value)) return
|
||||
|
||||
const title = getText(value.title) ?? getText(value.name) ?? getText(value.heading)
|
||||
const description = getText(value.description) ?? getText(value.body) ?? getText(value.text)
|
||||
|
||||
if (!title) return
|
||||
if (!description) return
|
||||
|
||||
const tag = getText(value.tag) ?? getText(value.label) ?? "New"
|
||||
|
||||
const media = (() => {
|
||||
const parsed = parseMedia(value.media)
|
||||
if (parsed) return parsed
|
||||
|
||||
const alt = getText(value.alt)
|
||||
const image = getText(value.image)
|
||||
if (image) return { type: "image" as const, src: normalizeRemoteUrl(image), alt }
|
||||
|
||||
const video = getText(value.video)
|
||||
if (video) return { type: "video" as const, src: normalizeRemoteUrl(video), alt }
|
||||
})()
|
||||
|
||||
return { title, description, tag, media }
|
||||
}
|
||||
|
||||
function parseChangelog(value: unknown): ReleaseNote | undefined {
|
||||
const releases = (() => {
|
||||
if (Array.isArray(value)) return value
|
||||
if (!isRecord(value)) return
|
||||
if (Array.isArray(value.releases)) return value.releases
|
||||
if (Array.isArray(value.versions)) return value.versions
|
||||
if (Array.isArray(value.changelog)) return value.changelog
|
||||
})()
|
||||
|
||||
if (!releases) {
|
||||
if (!isRecord(value)) return
|
||||
if (!Array.isArray(value.highlights)) return
|
||||
const features = value.highlights.map(parseFeature).filter((item): item is ReleaseFeature => item !== undefined)
|
||||
if (features.length === 0) return
|
||||
return { version: CURRENT_RELEASE.version, features: features.slice(0, 3) }
|
||||
}
|
||||
|
||||
const version = (() => {
|
||||
const head = releases[0]
|
||||
if (!isRecord(head)) return
|
||||
return getText(head.version) ?? getText(head.tag_name) ?? getText(head.tag) ?? getText(head.name)
|
||||
})()
|
||||
|
||||
const features = releases
|
||||
.flatMap((item) => {
|
||||
if (!isRecord(item)) return []
|
||||
const highlights = item.highlights
|
||||
if (!Array.isArray(highlights)) return []
|
||||
return highlights.map(parseFeature).filter((feature): feature is ReleaseFeature => feature !== undefined)
|
||||
})
|
||||
.slice(0, 3)
|
||||
|
||||
if (features.length === 0) return
|
||||
return { version: version ?? CURRENT_RELEASE.version, features }
|
||||
}
|
||||
|
||||
export interface ReleaseFeature {
|
||||
export type Highlight = {
|
||||
title: string
|
||||
description: string
|
||||
tag?: string
|
||||
media?: {
|
||||
type: "image" | "video"
|
||||
src: string
|
||||
@@ -116,74 +14,42 @@ export interface ReleaseFeature {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReleaseNote {
|
||||
version: string
|
||||
features: ReleaseFeature[]
|
||||
}
|
||||
|
||||
// Current release notes - update this with each release
|
||||
export const CURRENT_RELEASE: ReleaseNote = {
|
||||
version: "1.0.0",
|
||||
features: [
|
||||
{
|
||||
title: "Cleaner tab experience",
|
||||
description: "Chat is now fixed to the side of your tabs, and review is now available as a dedicated tab. ",
|
||||
tag: "New",
|
||||
media: {
|
||||
type: "video",
|
||||
src: "/release/release-example.mp4",
|
||||
alt: "Cleaner tab experience",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Share with control",
|
||||
description: "Keep your sessions private by default, or publish them to the web with a shareable URL.",
|
||||
tag: "New",
|
||||
media: {
|
||||
type: "image",
|
||||
src: "/release/release-share.png",
|
||||
alt: "Share with control",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Improved attachment management",
|
||||
description: "Upload and manage attachments more easily, to help build and maintain context.",
|
||||
tag: "New",
|
||||
media: {
|
||||
type: "video",
|
||||
src: "/release/release-example.mp4",
|
||||
alt: "Improved attachment management",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function DialogReleaseNotes(props: { release?: ReleaseNote }) {
|
||||
export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
|
||||
const dialog = useDialog()
|
||||
const [note, setNote] = createSignal(props.release ?? CURRENT_RELEASE)
|
||||
const settings = useSettings()
|
||||
const [index, setIndex] = createSignal(0)
|
||||
|
||||
const feature = () => note().features[index()] ?? note().features[0] ?? CURRENT_RELEASE.features[0]!
|
||||
const total = () => note().features.length
|
||||
const total = () => props.highlights.length
|
||||
const last = () => Math.max(0, total() - 1)
|
||||
const feature = () => props.highlights[index()] ?? props.highlights[last()]
|
||||
const isFirst = () => index() === 0
|
||||
const isLast = () => index() === total() - 1
|
||||
const isLast = () => index() >= last()
|
||||
const paged = () => total() > 1
|
||||
|
||||
function handleNext() {
|
||||
if (!isLast()) setIndex(index() + 1)
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
if (!isFirst()) setIndex(index() - 1)
|
||||
if (isLast()) return
|
||||
setIndex(index() + 1)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
markReleaseNotesSeen()
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
function handleDisable() {
|
||||
settings.general.setReleaseNotes(false)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
let focusTrap: HTMLDivElement | undefined
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
handleClose()
|
||||
return
|
||||
}
|
||||
|
||||
if (!paged()) return
|
||||
if (e.key === "ArrowLeft" && !isFirst()) {
|
||||
e.preventDefault()
|
||||
setIndex(index() - 1)
|
||||
@@ -198,26 +64,6 @@ export function DialogReleaseNotes(props: { release?: ReleaseNote }) {
|
||||
focusTrap?.focus()
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
onCleanup(() => document.removeEventListener("keydown", handleKeyDown))
|
||||
|
||||
const controller = new AbortController()
|
||||
fetch(CHANGELOG_URL, {
|
||||
signal: controller.signal,
|
||||
headers: { Accept: "application/json" },
|
||||
})
|
||||
.then((response) => (response.ok ? (response.json() as Promise<unknown>) : undefined))
|
||||
.then((json) => {
|
||||
if (!json) return
|
||||
const parsed = parseChangelog(json)
|
||||
if (!parsed) return
|
||||
setNote({
|
||||
version: parsed.version,
|
||||
features: parsed.features,
|
||||
})
|
||||
setIndex(0)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
|
||||
onCleanup(() => controller.abort())
|
||||
})
|
||||
|
||||
// Refocus the trap when index changes to ensure escape always works
|
||||
@@ -227,84 +73,86 @@ export function DialogReleaseNotes(props: { release?: ReleaseNote }) {
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog class="dialog-release-notes">
|
||||
<Dialog
|
||||
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 */}
|
||||
<div ref={focusTrap} tabindex="0" class="absolute opacity-0 pointer-events-none" />
|
||||
{/* Left side - Text content */}
|
||||
<div class="flex flex-col flex-1 min-w-0 p-8">
|
||||
{/* Top section - feature content (fixed position from top) */}
|
||||
<div class="flex flex-col gap-2 pt-22">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-16-medium text-text-strong">{feature().title}</h1>
|
||||
{feature().tag && (
|
||||
<span
|
||||
class="text-12-medium text-text-weak px-1.5 py-0.5 bg-surface-base rounded-sm border border-border-weak-base"
|
||||
style={{ "border-width": "0.5px" }}
|
||||
>
|
||||
{feature().tag}
|
||||
</span>
|
||||
)}
|
||||
</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 items-center gap-3">
|
||||
{isLast() ? (
|
||||
<Button variant="primary" size="large" onClick={handleClose}>
|
||||
Get started
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="secondary" size="large" onClick={handleNext}>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{total() > 1 && (
|
||||
<div class="flex items-center gap-1.5 -my-2.5">
|
||||
{note().features.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 class="flex flex-1 min-w-0 min-h-0">
|
||||
{/* Left side - Text content */}
|
||||
<div class="flex flex-col flex-1 min-w-0 p-8">
|
||||
{/* Top section - feature content (fixed position from top) */}
|
||||
<div class="flex flex-col gap-2 pt-22">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-16-medium text-text-strong">{feature()?.title ?? ""}</h1>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-14-regular text-text-base">{feature()?.description ?? ""}</p>
|
||||
</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 ?? "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" />
|
||||
)}
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,16 +24,18 @@ type Entry = {
|
||||
path?: string
|
||||
}
|
||||
|
||||
export function DialogSelectFile() {
|
||||
type DialogSelectFileMode = "all" | "files"
|
||||
|
||||
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const layout = useLayout()
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
const params = useParams()
|
||||
const filesOnly = () => props.mode === "files"
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const [grouped, setGrouped] = createSignal(false)
|
||||
const common = [
|
||||
@@ -46,11 +48,12 @@ export function DialogSelectFile() {
|
||||
]
|
||||
const limit = 5
|
||||
|
||||
const allowed = createMemo(() =>
|
||||
command.options.filter(
|
||||
const allowed = createMemo(() => {
|
||||
if (filesOnly()) return []
|
||||
return command.options.filter(
|
||||
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const commandItem = (option: CommandOption): Entry => ({
|
||||
id: "command:" + option.id,
|
||||
@@ -99,10 +102,50 @@ export function DialogSelectFile() {
|
||||
return items.slice(0, limit)
|
||||
})
|
||||
|
||||
const items = async (filter: string) => {
|
||||
const query = filter.trim()
|
||||
const root = createMemo(() => {
|
||||
const nodes = file.tree.children("")
|
||||
const paths = nodes
|
||||
.filter((node) => node.type === "file")
|
||||
.map((node) => node.path)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
return paths.slice(0, limit).map(fileItem)
|
||||
})
|
||||
|
||||
const unique = (items: Entry[]) => {
|
||||
const seen = new Set<string>()
|
||||
const out: Entry[] = []
|
||||
for (const item of items) {
|
||||
if (seen.has(item.id)) continue
|
||||
seen.add(item.id)
|
||||
out.push(item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const items = async (text: string) => {
|
||||
const query = text.trim()
|
||||
setGrouped(query.length > 0)
|
||||
|
||||
if (!query && filesOnly()) {
|
||||
const loaded = file.tree.state("")?.loaded
|
||||
const pending = loaded ? Promise.resolve() : file.tree.list("")
|
||||
const next = unique([...recent(), ...root()])
|
||||
|
||||
if (loaded || next.length > 0) {
|
||||
void pending
|
||||
return next
|
||||
}
|
||||
|
||||
await pending
|
||||
return unique([...recent(), ...root()])
|
||||
}
|
||||
|
||||
if (!query) return [...picks(), ...recent()]
|
||||
|
||||
if (filesOnly()) {
|
||||
const files = await file.searchFiles(query)
|
||||
return files.map(fileItem)
|
||||
}
|
||||
const files = await file.searchFiles(query)
|
||||
const entries = files.map(fileItem)
|
||||
return [...list(), ...entries]
|
||||
@@ -119,7 +162,9 @@ export function DialogSelectFile() {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("all")
|
||||
props.onOpenFile?.(path)
|
||||
}
|
||||
|
||||
const handleSelect = (item: Entry | undefined) => {
|
||||
@@ -143,13 +188,14 @@ export function DialogSelectFile() {
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog class="pt-3 pb-0 !max-h-[480px]">
|
||||
<Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
|
||||
<List
|
||||
search={{
|
||||
placeholder: language.t("palette.search.placeholder"),
|
||||
placeholder: filesOnly()
|
||||
? language.t("session.header.searchFiles")
|
||||
: language.t("palette.search.placeholder"),
|
||||
autofocus: true,
|
||||
hideIcon: true,
|
||||
class: "pl-3 pr-2 !mb-0",
|
||||
}}
|
||||
emptyMessage={language.t("palette.empty")}
|
||||
loadingMessage={language.t("common.loading")}
|
||||
@@ -177,7 +223,7 @@ export function DialogSelectFile() {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="w-full flex items-center justify-between gap-4 pl-1">
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
||||
<Show when={item.description}>
|
||||
|
||||
@@ -54,6 +54,7 @@ const ModelList: Component<{
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={12}
|
||||
forceMount={false}
|
||||
value={
|
||||
<ModelTooltip
|
||||
model={item}
|
||||
@@ -110,6 +111,11 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||
setStore("open", false)
|
||||
dialog.show(() => <DialogManageModels />)
|
||||
}
|
||||
|
||||
const handleConnectProvider = () => {
|
||||
setStore("open", false)
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
const language = useLanguage()
|
||||
|
||||
createEffect(() => {
|
||||
@@ -181,7 +187,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content
|
||||
ref={(el) => setStore("content", el)}
|
||||
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"
|
||||
class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
|
||||
onEscapeKeyDown={(event) => {
|
||||
setStore("dismiss", "escape")
|
||||
setStore("open", false)
|
||||
@@ -207,15 +213,28 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||
onSelect={() => setStore("open", false)}
|
||||
class="p-1"
|
||||
action={
|
||||
<IconButton
|
||||
icon="sliders"
|
||||
variant="ghost"
|
||||
iconSize="normal"
|
||||
class="size-6"
|
||||
aria-label={language.t("dialog.model.manage")}
|
||||
title={language.t("dialog.model.manage")}
|
||||
onClick={handleManage}
|
||||
/>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tooltip placement="top" forceMount={false} value={language.t("command.provider.connect")}>
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="normal"
|
||||
class="size-6"
|
||||
aria-label={language.t("command.provider.connect")}
|
||||
onClick={handleConnectProvider}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" forceMount={false} value={language.t("dialog.model.manage")}>
|
||||
<IconButton
|
||||
icon="sliders"
|
||||
variant="ghost"
|
||||
iconSize="normal"
|
||||
class="size-6"
|
||||
aria-label={language.t("dialog.model.manage")}
|
||||
onClick={handleManage}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Kobalte.Content>
|
||||
|
||||
@@ -5,9 +5,17 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
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 = () => {
|
||||
const dialog = useDialog()
|
||||
@@ -18,7 +26,7 @@ export const DialogSelectProvider: Component = () => {
|
||||
const otherGroup = () => language.t("dialog.provider.group.other")
|
||||
|
||||
return (
|
||||
<Dialog title={language.t("command.provider.connect")}>
|
||||
<Dialog title={language.t("command.provider.connect")} transition>
|
||||
<List
|
||||
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.provider.empty")}
|
||||
@@ -26,11 +34,13 @@ export const DialogSelectProvider: Component = () => {
|
||||
key={(x) => x?.id}
|
||||
items={() => {
|
||||
language.locale()
|
||||
return providers.all()
|
||||
return [{ id: CUSTOM_ID, name: "Custom provider" }, ...providers.all()]
|
||||
}}
|
||||
filterKeys={["id", "name"]}
|
||||
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
||||
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))
|
||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||
return a.name.localeCompare(b.name)
|
||||
@@ -43,13 +53,20 @@ export const DialogSelectProvider: Component = () => {
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
if (x.id === CUSTOM_ID) {
|
||||
dialog.show(() => <DialogCustomProvider back="providers" />)
|
||||
return
|
||||
}
|
||||
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="px-1.25 w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={icon(i.id)} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === CUSTOM_ID}>
|
||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||
</Show>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useLanguage } from "@/context/language"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
|
||||
type ServerStatus = { healthy: boolean; version?: string }
|
||||
|
||||
@@ -40,10 +41,11 @@ interface EditRowProps {
|
||||
}
|
||||
|
||||
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({
|
||||
baseUrl: url,
|
||||
fetch: platform.fetch,
|
||||
signal: AbortSignal.timeout(3000),
|
||||
signal,
|
||||
})
|
||||
return sdk.global
|
||||
.health()
|
||||
@@ -57,18 +59,16 @@ function AddRow(props: AddRowProps) {
|
||||
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full absolute left-3 z-10 pointer-events-none": true,
|
||||
"size-1.5 rounded-full absolute left-3 top-1/2 -translate-y-1/2 z-10 pointer-events-none": true,
|
||||
"bg-icon-success-base": props.status === true,
|
||||
"bg-icon-critical-base": props.status === false,
|
||||
"bg-border-weak-base": props.status === undefined,
|
||||
}}
|
||||
style={{ top: "50%", transform: "translateY(-50%)" }}
|
||||
ref={(el) => {
|
||||
// Position relative to input-wrapper
|
||||
requestAnimationFrame(() => {
|
||||
const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]')
|
||||
if (wrapper instanceof HTMLElement) {
|
||||
wrapper.style.position = "relative"
|
||||
wrapper.appendChild(el)
|
||||
}
|
||||
})
|
||||
@@ -149,13 +149,22 @@ export function DialogSelectServer() {
|
||||
})
|
||||
const [defaultUrl, defaultUrlActions] = createResource(
|
||||
async () => {
|
||||
const url = await platform.getDefaultServerUrl?.()
|
||||
if (!url) return null
|
||||
return normalizeServerUrl(url) ?? null
|
||||
try {
|
||||
const url = await platform.getDefaultServerUrl?.()
|
||||
if (!url) return null
|
||||
return normalizeServerUrl(url) ?? null
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
return null
|
||||
}
|
||||
},
|
||||
{ initialValue: null },
|
||||
)
|
||||
const isDesktop = platform.platform === "desktop"
|
||||
const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl)
|
||||
|
||||
const looksComplete = (value: string) => {
|
||||
const normalized = normalizeServerUrl(value)
|
||||
@@ -505,11 +514,19 @@ export function DialogSelectServer() {
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={isDesktop && defaultUrl() !== i}>
|
||||
<Show when={canDefault() && defaultUrl() !== i}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={async () => {
|
||||
await platform.setDefaultServerUrl?.(i)
|
||||
defaultUrlActions.mutate(i)
|
||||
try {
|
||||
await platform.setDefaultServerUrl?.(i)
|
||||
defaultUrlActions.mutate(i)
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
@@ -517,11 +534,19 @@ export function DialogSelectServer() {
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={isDesktop && defaultUrl() === i}>
|
||||
<Show when={canDefault() && defaultUrl() === i}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={async () => {
|
||||
await platform.setDefaultServerUrl?.(null)
|
||||
defaultUrlActions.mutate(null)
|
||||
try {
|
||||
await platform.setDefaultServerUrl?.(null)
|
||||
defaultUrlActions.mutate(null)
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
|
||||
@@ -6,19 +6,15 @@ import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { SettingsGeneral } from "./settings-general"
|
||||
import { SettingsKeybinds } from "./settings-keybinds"
|
||||
import { SettingsPermissions } from "./settings-permissions"
|
||||
import { SettingsProviders } from "./settings-providers"
|
||||
import { SettingsModels } from "./settings-models"
|
||||
import { SettingsAgents } from "./settings-agents"
|
||||
import { SettingsCommands } from "./settings-commands"
|
||||
import { SettingsMcp } from "./settings-mcp"
|
||||
|
||||
export const DialogSettings: Component = () => {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
|
||||
return (
|
||||
<Dialog size="x-large">
|
||||
<Dialog size="x-large" transition>
|
||||
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
||||
<Tabs.List>
|
||||
<div class="flex flex-col justify-between h-full w-full">
|
||||
@@ -42,15 +38,19 @@ export const DialogSettings: Component = () => {
|
||||
<Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
|
||||
<div class="flex flex-col gap-1.5 w-full">
|
||||
<Tabs.Trigger value="providers">
|
||||
<Icon name="server" />
|
||||
<Icon name="providers" />
|
||||
{language.t("settings.providers.title")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="models">
|
||||
<Icon name="models" />
|
||||
{language.t("settings.models.title")}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
|
||||
<span>OpenCode Desktop</span>
|
||||
<span>{language.t("app.name.desktop")}</span>
|
||||
<span class="text-11-regular">v{platform.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,9 +64,9 @@ export const DialogSettings: Component = () => {
|
||||
<Tabs.Content value="providers" class="no-scrollbar">
|
||||
<SettingsProviders />
|
||||
</Tabs.Content>
|
||||
{/* <Tabs.Content value="models" class="no-scrollbar"> */}
|
||||
{/* <SettingsModels /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
<Tabs.Content value="models" class="no-scrollbar">
|
||||
<SettingsModels />
|
||||
</Tabs.Content>
|
||||
{/* <Tabs.Content value="agents" class="no-scrollbar"> */}
|
||||
{/* <SettingsAgents /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
|
||||
@@ -1,111 +1,373 @@
|
||||
import { useLocal, type LocalFile } from "@/context/local"
|
||||
import { useFile } from "@/context/file"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js"
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
splitProps,
|
||||
Switch,
|
||||
untrack,
|
||||
type ComponentProps,
|
||||
type ParentProps,
|
||||
} from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||
|
||||
type Kind = "add" | "del" | "mix"
|
||||
|
||||
type Filter = {
|
||||
files: Set<string>
|
||||
dirs: Set<string>
|
||||
}
|
||||
|
||||
export default function FileTree(props: {
|
||||
path: string
|
||||
class?: string
|
||||
nodeClass?: string
|
||||
active?: string
|
||||
level?: number
|
||||
onFileClick?: (file: LocalFile) => void
|
||||
allowed?: readonly string[]
|
||||
modified?: readonly string[]
|
||||
kinds?: ReadonlyMap<string, Kind>
|
||||
draggable?: boolean
|
||||
tooltip?: boolean
|
||||
onFileClick?: (file: FileNode) => void
|
||||
|
||||
_filter?: Filter
|
||||
_marks?: Set<string>
|
||||
_deeps?: Map<string, number>
|
||||
_kinds?: ReadonlyMap<string, Kind>
|
||||
}) {
|
||||
const local = useLocal()
|
||||
const file = useFile()
|
||||
const level = props.level ?? 0
|
||||
const draggable = () => props.draggable ?? true
|
||||
const tooltip = () => props.tooltip ?? true
|
||||
|
||||
const Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => (
|
||||
<Dynamic
|
||||
component={p.as ?? "div"}
|
||||
classList={{
|
||||
"p-0.5 w-full flex items-center gap-x-2 hover:bg-background-element": true,
|
||||
// "bg-background-element": local.file.active()?.path === p.node.path,
|
||||
[props.nodeClass ?? ""]: !!props.nodeClass,
|
||||
}}
|
||||
style={`padding-left: ${level * 10}px`}
|
||||
draggable={true}
|
||||
onDragStart={(e: any) => {
|
||||
const evt = e as globalThis.DragEvent
|
||||
evt.dataTransfer!.effectAllowed = "copy"
|
||||
evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`)
|
||||
const filter = createMemo(() => {
|
||||
if (props._filter) return props._filter
|
||||
|
||||
// Create custom drag image without margins
|
||||
const dragImage = document.createElement("div")
|
||||
dragImage.className =
|
||||
"flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1"
|
||||
dragImage.style.position = "absolute"
|
||||
dragImage.style.top = "-1000px"
|
||||
const allowed = props.allowed
|
||||
if (!allowed) return
|
||||
|
||||
// Copy only the icon and text content without padding
|
||||
const icon = e.currentTarget.querySelector("svg")
|
||||
const text = e.currentTarget.querySelector("span")
|
||||
if (icon && text) {
|
||||
dragImage.innerHTML = icon.outerHTML + text.outerHTML
|
||||
}
|
||||
const files = new Set(allowed)
|
||||
const dirs = new Set<string>()
|
||||
|
||||
document.body.appendChild(dragImage)
|
||||
evt.dataTransfer!.setDragImage(dragImage, 0, 12)
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0)
|
||||
}}
|
||||
{...p}
|
||||
>
|
||||
{p.children}
|
||||
<span
|
||||
for (const item of allowed) {
|
||||
const parts = item.split("/")
|
||||
const parents = parts.slice(0, -1)
|
||||
for (const [idx] of parents.entries()) {
|
||||
const dir = parents.slice(0, idx + 1).join("/")
|
||||
if (dir) dirs.add(dir)
|
||||
}
|
||||
}
|
||||
|
||||
return { files, dirs }
|
||||
})
|
||||
|
||||
const marks = createMemo(() => {
|
||||
if (props._marks) return props._marks
|
||||
|
||||
const out = new Set<string>()
|
||||
for (const item of props.modified ?? []) out.add(item)
|
||||
for (const item of props.kinds?.keys() ?? []) out.add(item)
|
||||
if (out.size === 0) return
|
||||
return out
|
||||
})
|
||||
|
||||
const kinds = createMemo(() => {
|
||||
if (props._kinds) return props._kinds
|
||||
return props.kinds
|
||||
})
|
||||
|
||||
const deeps = createMemo(() => {
|
||||
if (props._deeps) return props._deeps
|
||||
|
||||
const out = new Map<string, number>()
|
||||
|
||||
const visit = (dir: string, lvl: number): number => {
|
||||
const expanded = file.tree.state(dir)?.expanded ?? false
|
||||
if (!expanded) return -1
|
||||
|
||||
const nodes = file.tree.children(dir)
|
||||
const max = nodes.reduce((max, node) => {
|
||||
if (node.type !== "directory") return max
|
||||
const open = file.tree.state(node.path)?.expanded ?? false
|
||||
if (!open) return max
|
||||
return Math.max(max, visit(node.path, lvl + 1))
|
||||
}, lvl)
|
||||
|
||||
out.set(dir, max)
|
||||
return max
|
||||
}
|
||||
|
||||
visit(props.path, level - 1)
|
||||
return out
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const current = filter()
|
||||
if (!current) return
|
||||
if (level !== 0) return
|
||||
|
||||
for (const dir of current.dirs) {
|
||||
const expanded = untrack(() => file.tree.state(dir)?.expanded) ?? false
|
||||
if (expanded) continue
|
||||
file.tree.expand(dir)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const path = props.path
|
||||
untrack(() => void file.tree.list(path))
|
||||
})
|
||||
|
||||
const nodes = createMemo(() => {
|
||||
const nodes = file.tree.children(props.path)
|
||||
const current = filter()
|
||||
if (!current) return nodes
|
||||
return nodes.filter((node) => {
|
||||
if (node.type === "file") return current.files.has(node.path)
|
||||
return current.dirs.has(node.path)
|
||||
})
|
||||
})
|
||||
|
||||
const Node = (
|
||||
p: ParentProps &
|
||||
ComponentProps<"div"> &
|
||||
ComponentProps<"button"> & {
|
||||
node: FileNode
|
||||
as?: "div" | "button"
|
||||
},
|
||||
) => {
|
||||
const [local, rest] = splitProps(p, ["node", "as", "children", "class", "classList"])
|
||||
return (
|
||||
<Dynamic
|
||||
component={local.as ?? "div"}
|
||||
classList={{
|
||||
"text-xs whitespace-nowrap truncate": true,
|
||||
"text-text-muted/40": p.node.ignored,
|
||||
"text-text-muted/80": !p.node.ignored,
|
||||
// "!text-text": local.file.active()?.path === p.node.path,
|
||||
// "!text-primary": local.file.changed(p.node.path),
|
||||
"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,
|
||||
"bg-surface-base-active": local.node.path === props.active,
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
[props.nodeClass ?? ""]: !!props.nodeClass,
|
||||
}}
|
||||
style={`padding-left: ${Math.max(0, 8 + level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
|
||||
draggable={draggable()}
|
||||
onDragStart={(e: DragEvent) => {
|
||||
if (!draggable()) return
|
||||
e.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
|
||||
e.dataTransfer?.setData("text/uri-list", `file://${local.node.path}`)
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy"
|
||||
|
||||
const dragImage = document.createElement("div")
|
||||
dragImage.className =
|
||||
"flex items-center gap-x-2 px-2 py-1 bg-surface-raised-base rounded-md border border-border-base text-12-regular text-text-strong"
|
||||
dragImage.style.position = "absolute"
|
||||
dragImage.style.top = "-1000px"
|
||||
|
||||
const icon =
|
||||
(e.currentTarget as HTMLElement).querySelector('[data-component="file-icon"]') ??
|
||||
(e.currentTarget as HTMLElement).querySelector("svg")
|
||||
const text = (e.currentTarget as HTMLElement).querySelector("span")
|
||||
if (icon && text) {
|
||||
dragImage.innerHTML = (icon as SVGElement).outerHTML + (text as HTMLSpanElement).outerHTML
|
||||
}
|
||||
|
||||
document.body.appendChild(dragImage)
|
||||
e.dataTransfer?.setDragImage(dragImage, 0, 12)
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0)
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{p.node.name}
|
||||
</span>
|
||||
{/* <Show when={local.file.changed(p.node.path)}> */}
|
||||
{/* <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" /> */}
|
||||
{/* </Show> */}
|
||||
</Dynamic>
|
||||
)
|
||||
{local.children}
|
||||
{(() => {
|
||||
const kind = kinds()?.get(local.node.path)
|
||||
const marked = marks()?.has(local.node.path) ?? false
|
||||
const active = !!kind && marked && !local.node.ignored
|
||||
const color =
|
||||
kind === "add"
|
||||
? "color: var(--icon-diff-add-base)"
|
||||
: kind === "del"
|
||||
? "color: var(--icon-diff-delete-base)"
|
||||
: kind === "mix"
|
||||
? "color: var(--icon-diff-modified-base)"
|
||||
: undefined
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={`flex flex-col ${props.class}`}>
|
||||
<For each={local.file.children(props.path)}>
|
||||
{(node) => (
|
||||
<Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right">
|
||||
<div class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
||||
<For each={nodes()}>
|
||||
{(node) => {
|
||||
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
||||
const deep = () => deeps().get(node.path) ?? -1
|
||||
const Wrapper = (p: ParentProps) => {
|
||||
if (!tooltip()) return p.children
|
||||
|
||||
const parts = node.path.split("/")
|
||||
const leaf = parts[parts.length - 1] ?? node.path
|
||||
const head = parts.slice(0, -1).join("/")
|
||||
const prefix = head ? `${head}/` : ""
|
||||
|
||||
const kind = () => kinds()?.get(node.path)
|
||||
const label = () => {
|
||||
const k = kind()
|
||||
if (!k) return
|
||||
if (k === "add") return "Additions"
|
||||
if (k === "del") return "Deletions"
|
||||
return "Modifications"
|
||||
}
|
||||
|
||||
const ignored = () => node.type === "directory" && node.ignored
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
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}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={node.type === "directory"}>
|
||||
<Collapsible
|
||||
variant="ghost"
|
||||
class="w-full"
|
||||
data-scope="filetree"
|
||||
forceMount={false}
|
||||
// open={local.file.node(node.path)?.expanded}
|
||||
onOpenChange={(open) => (open ? local.file.expand(node.path) : local.file.collapse(node.path))}
|
||||
open={expanded()}
|
||||
onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
|
||||
>
|
||||
<Collapsible.Trigger>
|
||||
<Node node={node}>
|
||||
<Collapsible.Arrow class="text-text-muted/60 ml-1" />
|
||||
<FileIcon
|
||||
node={node}
|
||||
// expanded={local.file.node(node.path).expanded}
|
||||
class="text-text-muted/60 -ml-1"
|
||||
/>
|
||||
</Node>
|
||||
<Wrapper>
|
||||
<Node node={node}>
|
||||
<div class="size-4 flex items-center justify-center text-icon-weak">
|
||||
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
|
||||
</div>
|
||||
</Node>
|
||||
</Wrapper>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<FileTree path={node.path} level={level + 1} onFileClick={props.onFileClick} />
|
||||
<Collapsible.Content class="relative pt-0.5">
|
||||
<div
|
||||
classList={{
|
||||
"absolute top-0 bottom-0 w-px pointer-events-none bg-border-weak-base opacity-0 transition-opacity duration-150 ease-out motion-reduce:transition-none": true,
|
||||
"group-hover/filetree:opacity-100": expanded() && deep() === level,
|
||||
"group-hover/filetree:opacity-50": !(expanded() && deep() === level),
|
||||
}}
|
||||
style={`left: ${Math.max(0, 8 + level * 12 - 4) + 8}px`}
|
||||
/>
|
||||
<FileTree
|
||||
path={node.path}
|
||||
level={level + 1}
|
||||
allowed={props.allowed}
|
||||
modified={props.modified}
|
||||
kinds={props.kinds}
|
||||
active={props.active}
|
||||
draggable={props.draggable}
|
||||
tooltip={props.tooltip}
|
||||
onFileClick={props.onFileClick}
|
||||
_filter={filter()}
|
||||
_marks={marks()}
|
||||
_deeps={deeps()}
|
||||
_kinds={kinds()}
|
||||
/>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
</Match>
|
||||
<Match when={node.type === "file"}>
|
||||
<Node node={node} as="button" onClick={() => props.onFileClick?.(node)}>
|
||||
<div class="w-4 shrink-0" />
|
||||
<FileIcon node={node} class="text-primary" />
|
||||
</Node>
|
||||
<Wrapper>
|
||||
<Node node={node} as="button" type="button" onClick={() => props.onFileClick?.(node)}>
|
||||
<div class="w-4 shrink-0" />
|
||||
<FileIcon node={node} class="text-icon-weak size-4" />
|
||||
</Node>
|
||||
</Wrapper>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tooltip>
|
||||
)}
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -171,7 +171,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
|
||||
const commentInReview = (path: string) => {
|
||||
const sessionID = params.id
|
||||
@@ -187,20 +186,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
const focus = { file: item.path, id: item.commentID }
|
||||
comments.setActive(focus)
|
||||
view().reviewPanel.open()
|
||||
|
||||
if (item.commentOrigin === "review") {
|
||||
tabs().open("review")
|
||||
requestAnimationFrame(() => comments.setFocus(focus))
|
||||
return
|
||||
}
|
||||
|
||||
if (item.commentOrigin !== "file" && commentInReview(item.path)) {
|
||||
tabs().open("review")
|
||||
const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
|
||||
if (wantsReview) {
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("changes")
|
||||
requestAnimationFrame(() => comments.setFocus(focus))
|
||||
return
|
||||
}
|
||||
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("all")
|
||||
const tab = files.tab(item.path)
|
||||
tabs().open(tab)
|
||||
files.load(item.path)
|
||||
@@ -1042,13 +1038,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
|
||||
|
||||
if (store.popover) {
|
||||
if (event.key === "Tab") {
|
||||
selectPopoverActive()
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter") {
|
||||
const nav = 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") {
|
||||
atOnKeyDown(event)
|
||||
event.preventDefault()
|
||||
@@ -1062,8 +1062,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
|
||||
|
||||
if (ctrl && event.code === "KeyG") {
|
||||
if (store.popover) {
|
||||
setStore("popover", null)
|
||||
@@ -1563,13 +1561,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
|
||||
const timeoutMs = 5 * 60 * 1000
|
||||
const timer = { id: undefined as number | undefined }
|
||||
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ status: "failed", message: "Workspace is still preparing" })
|
||||
timer.id = window.setTimeout(() => {
|
||||
resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") })
|
||||
}, timeoutMs)
|
||||
})
|
||||
|
||||
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout])
|
||||
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout]).finally(() => {
|
||||
if (timer.id === undefined) return
|
||||
clearTimeout(timer.id)
|
||||
})
|
||||
pending.delete(session.id)
|
||||
if (controller.signal.aborted) return false
|
||||
if (result.status === "failed") throw new Error(result.message)
|
||||
@@ -1746,10 +1748,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Tooltip
|
||||
value={
|
||||
<span class="flex max-w-[300px]">
|
||||
<span
|
||||
class="text-text-invert-base truncate min-w-0"
|
||||
style={{ direction: "rtl", "text-align": "left", "unicode-bidi": "plaintext" }}
|
||||
>
|
||||
<span class="text-text-invert-base truncate-start [unicode-bidi:plaintext] min-w-0">
|
||||
{getDirectory(item.path)}
|
||||
</span>
|
||||
<span class="shrink-0">{getFilename(item.path)}</span>
|
||||
@@ -1772,10 +1771,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
>
|
||||
<div class="flex items-center gap-1.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"
|
||||
style={{ "font-weight": "var(--font-weight-medium)" }}
|
||||
>
|
||||
<div class="flex items-center text-11-regular min-w-0 font-medium">
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
|
||||
<Show when={item.selection}>
|
||||
{(sel) => (
|
||||
@@ -1863,9 +1859,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
store.mode === "shell"
|
||||
? language.t("prompt.placeholder.shell")
|
||||
: commentCount() > 1
|
||||
? "Summarize comments…"
|
||||
? language.t("prompt.placeholder.summarizeComments")
|
||||
: commentCount() === 1
|
||||
? "Summarize comment…"
|
||||
? language.t("prompt.placeholder.summarizeComment")
|
||||
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })
|
||||
}
|
||||
contenteditable="true"
|
||||
@@ -1887,9 +1883,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
{store.mode === "shell"
|
||||
? language.t("prompt.placeholder.shell")
|
||||
: commentCount() > 1
|
||||
? "Summarize comments…"
|
||||
? language.t("prompt.placeholder.summarizeComments")
|
||||
: commentCount() === 1
|
||||
? "Summarize comment…"
|
||||
? language.t("prompt.placeholder.summarizeComment")
|
||||
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { onMount } from "solid-js"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogReleaseNotes } from "./dialog-release-notes"
|
||||
import { shouldShowReleaseNotes, markReleaseNotesSeen } from "@/lib/release-notes"
|
||||
|
||||
/**
|
||||
* Component that handles showing release notes modal on app startup.
|
||||
* Shows the modal if:
|
||||
* - DEV_ALWAYS_SHOW_RELEASE_NOTES is true in lib/release-notes.ts
|
||||
* - OR the user hasn't seen the current version's release notes yet
|
||||
*
|
||||
* To disable the dev mode behavior, set DEV_ALWAYS_SHOW_RELEASE_NOTES to false
|
||||
* in packages/app/src/lib/release-notes.ts
|
||||
*/
|
||||
export function ReleaseNotesHandler() {
|
||||
const dialog = useDialog()
|
||||
|
||||
onMount(() => {
|
||||
// Small delay to ensure app is fully loaded before showing modal
|
||||
setTimeout(() => {
|
||||
if (shouldShowReleaseNotes()) {
|
||||
dialog.show(
|
||||
() => <DialogReleaseNotes />,
|
||||
() => markReleaseNotesSeen(),
|
||||
)
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -23,7 +23,6 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
const variant = createMemo(() => props.variant ?? "button")
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
|
||||
const usd = createMemo(
|
||||
@@ -58,7 +57,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
|
||||
const openContext = () => {
|
||||
if (!params.id) return
|
||||
view().reviewPanel.open()
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("all")
|
||||
tabs().open("context")
|
||||
tabs().setActive("context")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { usePlatform } from "@/context/platform"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
@@ -29,7 +29,7 @@ export function SessionHeader() {
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
|
||||
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||
const project = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return
|
||||
@@ -45,7 +45,6 @@ export function SessionHeader() {
|
||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const showShare = createMemo(() => shareEnabled() && !!currentSession())
|
||||
const showReview = createMemo(() => !!currentSession())
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
|
||||
@@ -131,7 +130,7 @@ export function SessionHeader() {
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
aria-label={language.t("session.header.searchFiles")}
|
||||
>
|
||||
@@ -284,28 +283,27 @@ export function SessionHeader() {
|
||||
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/review-toggle size-6 p-0"
|
||||
onClick={() => view().reviewPanel.toggle()}
|
||||
class="group/file-tree-toggle size-6 p-0"
|
||||
onClick={() => layout.fileTree.toggle()}
|
||||
aria-label={language.t("command.review.toggle")}
|
||||
aria-expanded={view().reviewPanel.opened()}
|
||||
aria-expanded={layout.fileTree.opened()}
|
||||
aria-controls="review-panel"
|
||||
tabIndex={showReview() ? 0 : -1}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
|
||||
class="group-hover/review-toggle:hidden"
|
||||
name={layout.fileTree.opened() ? "layout-right-full" : "layout-right"}
|
||||
class="group-hover/file-tree-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-right-partial"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
class="hidden group-hover/file-tree-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
name={layout.fileTree.opened() ? "layout-right" : "layout-right-full"}
|
||||
class="hidden group-active/file-tree-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -45,10 +45,7 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<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="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="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
|
||||
|
||||
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
||||
return (
|
||||
<div class="flex items-center gap-x-1.5">
|
||||
<div class="flex items-center gap-x-1.5 min-w-0">
|
||||
<FileIcon
|
||||
node={{ path: props.path, type: "file" }}
|
||||
classList={{
|
||||
@@ -19,7 +19,7 @@ export function FileVisual(props: { path: string; active?: boolean }): JSX.Eleme
|
||||
"grayscale-0": props.active,
|
||||
}}
|
||||
/>
|
||||
<span class="text-14-medium">{getFilename(props.path)}</span>
|
||||
<span class="text-14-medium truncate">{getFilename(props.path)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -38,8 +38,9 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
||||
closeButton={
|
||||
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||
<IconButton
|
||||
icon="close"
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
class="h-5 w-5"
|
||||
onClick={() => props.onTabClose(props.tab)}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { JSX } from "solid-js"
|
||||
import { createSignal, Show } from "solid-js"
|
||||
import { Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
@@ -12,11 +13,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
||||
const terminal = useTerminal()
|
||||
const language = useLanguage()
|
||||
const sortable = createSortable(props.terminal.id)
|
||||
const [editing, setEditing] = createSignal(false)
|
||||
const [title, setTitle] = createSignal(props.terminal.title)
|
||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
||||
const [menuPosition, setMenuPosition] = createSignal({ x: 0, y: 0 })
|
||||
const [blurEnabled, setBlurEnabled] = createSignal(false)
|
||||
const [store, setStore] = createStore({
|
||||
editing: false,
|
||||
title: props.terminal.title,
|
||||
menuOpen: false,
|
||||
menuPosition: { x: 0, y: 0 },
|
||||
blurEnabled: false,
|
||||
})
|
||||
|
||||
const isDefaultTitle = () => {
|
||||
const number = props.terminal.titleNumber
|
||||
@@ -47,7 +50,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
||||
}
|
||||
|
||||
const focus = () => {
|
||||
if (editing()) return
|
||||
if (store.editing) return
|
||||
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur()
|
||||
@@ -71,26 +74,26 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
setBlurEnabled(false)
|
||||
setTitle(props.terminal.title)
|
||||
setEditing(true)
|
||||
setStore("blurEnabled", false)
|
||||
setStore("title", props.terminal.title)
|
||||
setStore("editing", true)
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement
|
||||
if (!input) return
|
||||
input.focus()
|
||||
input.select()
|
||||
setTimeout(() => setBlurEnabled(true), 100)
|
||||
setTimeout(() => setStore("blurEnabled", true), 100)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
if (!blurEnabled()) return
|
||||
if (!store.blurEnabled) return
|
||||
|
||||
const value = title().trim()
|
||||
const value = store.title.trim()
|
||||
if (value && value !== props.terminal.title) {
|
||||
terminal.update({ id: props.terminal.id, title: value })
|
||||
}
|
||||
setEditing(false)
|
||||
setStore("editing", false)
|
||||
}
|
||||
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
@@ -101,14 +104,14 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setEditing(false)
|
||||
setStore("editing", false)
|
||||
}
|
||||
}
|
||||
|
||||
const menu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
setMenuPosition({ x: e.clientX, y: e.clientY })
|
||||
setMenuOpen(true)
|
||||
setStore("menuPosition", { x: e.clientX, y: e.clientY })
|
||||
setStore("menuOpen", true)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -143,17 +146,17 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span onDblClick={edit} style={{ visibility: editing() ? "hidden" : "visible" }}>
|
||||
<span onDblClick={edit} classList={{ invisible: store.editing }}>
|
||||
{label()}
|
||||
</span>
|
||||
</Tabs.Trigger>
|
||||
<Show when={editing()}>
|
||||
<Show when={store.editing}>
|
||||
<div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto">
|
||||
<input
|
||||
id={`terminal-title-input-${props.terminal.id}`}
|
||||
type="text"
|
||||
value={title()}
|
||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
||||
value={store.title}
|
||||
onInput={(e) => setStore("title", e.currentTarget.value)}
|
||||
onBlur={save}
|
||||
onKeyDown={keydown}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
@@ -161,13 +164,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
|
||||
<DropdownMenu open={store.menuOpen} onOpenChange={(open) => setStore("menuOpen", open)}>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
class="fixed"
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: `${menuPosition().x}px`,
|
||||
top: `${menuPosition().y}px`,
|
||||
left: `${store.menuPosition.x}px`,
|
||||
top: `${store.menuPosition.y}px`,
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Item onSelect={edit}>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
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 { Switch } from "@opencode-ai/ui/switch"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
||||
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
|
||||
import { Link } from "./link"
|
||||
@@ -29,8 +33,67 @@ const playDemoSound = (src: string) => {
|
||||
export const SettingsGeneral: Component = () => {
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
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(() =>
|
||||
Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
|
||||
)
|
||||
@@ -67,14 +130,8 @@ export const SettingsGeneral: Component = () => {
|
||||
const soundOptions = [...SOUND_OPTIONS]
|
||||
|
||||
return (
|
||||
<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"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<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">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
|
||||
</div>
|
||||
@@ -292,6 +349,50 @@ export const SettingsGeneral: Component = () => {
|
||||
</SettingsRow>
|
||||
</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>
|
||||
)
|
||||
@@ -305,8 +406,8 @@ interface SettingsRowProps {
|
||||
|
||||
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
||||
return (
|
||||
<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">
|
||||
<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 flex-col gap-0.5 min-w-0">
|
||||
<span class="text-14-medium text-text-strong">{props.title}</span>
|
||||
<span class="text-12-regular text-text-weak">{props.description}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
@@ -111,24 +112,26 @@ export const SettingsKeybinds: Component = () => {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
|
||||
const [active, setActive] = createSignal<string | null>(null)
|
||||
const [filter, setFilter] = createSignal("")
|
||||
const [store, setStore] = createStore({
|
||||
active: null as string | null,
|
||||
filter: "",
|
||||
})
|
||||
|
||||
const stop = () => {
|
||||
if (!active()) return
|
||||
setActive(null)
|
||||
if (!store.active) return
|
||||
setStore("active", null)
|
||||
command.keybinds(true)
|
||||
}
|
||||
|
||||
const start = (id: string) => {
|
||||
if (active() === id) {
|
||||
if (store.active === id) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
if (active()) stop()
|
||||
if (store.active) stop()
|
||||
|
||||
setActive(id)
|
||||
setStore("active", id)
|
||||
command.keybinds(false)
|
||||
}
|
||||
|
||||
@@ -203,7 +206,7 @@ export const SettingsKeybinds: Component = () => {
|
||||
})
|
||||
|
||||
const filtered = createMemo(() => {
|
||||
const query = filter().toLowerCase().trim()
|
||||
const query = store.filter.toLowerCase().trim()
|
||||
if (!query) return grouped()
|
||||
|
||||
const map = list()
|
||||
@@ -285,7 +288,7 @@ export const SettingsKeybinds: Component = () => {
|
||||
|
||||
onMount(() => {
|
||||
const handle = (event: KeyboardEvent) => {
|
||||
const id = active()
|
||||
const id = store.active
|
||||
if (!id) return
|
||||
|
||||
event.preventDefault()
|
||||
@@ -345,18 +348,12 @@ export const SettingsKeybinds: Component = () => {
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (active()) command.keybinds(true)
|
||||
if (store.active) command.keybinds(true)
|
||||
})
|
||||
|
||||
return (
|
||||
<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"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<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-4 pt-6 pb-6 max-w-[720px]">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
||||
@@ -370,8 +367,8 @@ export const SettingsKeybinds: Component = () => {
|
||||
<TextField
|
||||
variant="ghost"
|
||||
type="text"
|
||||
value={filter()}
|
||||
onChange={setFilter}
|
||||
value={store.filter}
|
||||
onChange={(v) => setStore("filter", v)}
|
||||
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
@@ -379,8 +376,8 @@ export const SettingsKeybinds: Component = () => {
|
||||
autocapitalize="off"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Show when={filter()}>
|
||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setFilter("")} />
|
||||
<Show when={store.filter}>
|
||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
@@ -402,13 +399,13 @@ export const SettingsKeybinds: Component = () => {
|
||||
classList={{
|
||||
"h-8 px-3 rounded-md text-12-regular": true,
|
||||
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
||||
active() !== id,
|
||||
"border border-border-weak-base bg-surface-inset-base text-text-weak": active() === id,
|
||||
store.active !== id,
|
||||
"border border-border-weak-base bg-surface-inset-base text-text-weak": store.active === id,
|
||||
}}
|
||||
onClick={() => start(id)}
|
||||
>
|
||||
<Show
|
||||
when={active() === id}
|
||||
when={store.active === id}
|
||||
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
||||
>
|
||||
{language.t("settings.shortcuts.pressKeys")}
|
||||
@@ -423,11 +420,11 @@ export const SettingsKeybinds: Component = () => {
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={filter() && !hasResults()}>
|
||||
<Show when={store.filter && !hasResults()}>
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
|
||||
<Show when={filter()}>
|
||||
<span class="text-14-regular text-text-strong mt-1">"{filter()}"</span>
|
||||
<Show when={store.filter}>
|
||||
<span class="text-14-regular text-text-strong mt-1">"{store.filter}"</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -1,14 +1,129 @@
|
||||
import { Component } from "solid-js"
|
||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { type Component, For, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useModels } from "@/context/models"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
|
||||
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
||||
|
||||
export const SettingsModels: Component = () => {
|
||||
const language = useLanguage()
|
||||
const models = useModels()
|
||||
|
||||
const list = useFilteredList<ModelItem>({
|
||||
items: (_filter) => models.list(),
|
||||
key: (x) => `${x.provider.id}:${x.id}`,
|
||||
filterKeys: ["provider.name", "name", "id"],
|
||||
sortBy: (a, b) => a.name.localeCompare(b.name),
|
||||
groupBy: (x) => x.provider.id,
|
||||
sortGroupsBy: (a, b) => {
|
||||
const aIndex = popularProviders.indexOf(a.category)
|
||||
const bIndex = popularProviders.indexOf(b.category)
|
||||
const aPopular = aIndex >= 0
|
||||
const bPopular = bIndex >= 0
|
||||
|
||||
if (aPopular && !bPopular) return -1
|
||||
if (!aPopular && bPopular) return 1
|
||||
if (aPopular && bPopular) return aIndex - bIndex
|
||||
|
||||
const aName = a.items[0].provider.name
|
||||
const bName = b.items[0].provider.name
|
||||
return aName.localeCompare(bName)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.models.description")}</p>
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<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-4 pt-6 pb-6 max-w-[720px]">
|
||||
<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">
|
||||
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
||||
<TextField
|
||||
variant="ghost"
|
||||
type="text"
|
||||
value={list.filter()}
|
||||
onChange={list.onInput}
|
||||
placeholder={language.t("dialog.model.search.placeholder")}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Show when={list.filter()}>
|
||||
<IconButton icon="circle-x" variant="ghost" onClick={list.clear} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8 max-w-[720px]">
|
||||
<Show
|
||||
when={!list.grouped.loading}
|
||||
fallback={
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<span class="text-14-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
{language.t("common.loading.ellipsis")}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={list.flat().length > 0}
|
||||
fallback={
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<span class="text-14-regular text-text-weak">{language.t("dialog.model.empty")}</span>
|
||||
<Show when={list.filter()}>
|
||||
<span class="text-14-regular text-text-strong mt-1">"{list.filter()}"</span>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={list.grouped.latest}>
|
||||
{(group) => (
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<ProviderIcon id={group.category as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
|
||||
</div>
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<For each={group.items}>
|
||||
{(item) => {
|
||||
const key = { providerID: item.provider.id, modelID: item.id }
|
||||
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="min-w-0">
|
||||
<span class="text-14-regular text-text-strong truncate block">{item.name}</span>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<Switch
|
||||
checked={models.visible(key)}
|
||||
onChange={(checked) => {
|
||||
models.setVisibility(key, checked)
|
||||
}}
|
||||
hideLabel
|
||||
>
|
||||
{item.name}
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -175,20 +175,14 @@ export const SettingsPermissions: Component = () => {
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
|
||||
<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 p-8 max-w-[720px]">
|
||||
<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 px-4 py-8 sm:p-8 max-w-[720px]">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6 p-8 pt-6 max-w-[720px]">
|
||||
<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-2">
|
||||
<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">
|
||||
@@ -223,8 +217,8 @@ interface SettingsRowProps {
|
||||
|
||||
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
||||
return (
|
||||
<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">
|
||||
<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 flex-col gap-0.5 min-w-0">
|
||||
<span class="text-14-medium text-text-strong">{props.title}</span>
|
||||
<span class="text-12-regular text-text-weak">{props.description}</span>
|
||||
</div>
|
||||
|
||||
@@ -3,13 +3,15 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { createMemo, type Component, For, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||
|
||||
type ProviderSource = "env" | "api" | "config" | "custom"
|
||||
type ProviderMeta = { source?: ProviderSource }
|
||||
@@ -18,9 +20,20 @@ export const SettingsProviders: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const providers = useProviders()
|
||||
|
||||
const connected = createMemo(() => providers.connected())
|
||||
const icon = (id: string): IconName => {
|
||||
if (iconNames.includes(id as IconName)) return id as IconName
|
||||
return "synthetic"
|
||||
}
|
||||
|
||||
const connected = createMemo(() => {
|
||||
return providers
|
||||
.connected()
|
||||
.filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input))
|
||||
})
|
||||
|
||||
const popular = createMemo(() => {
|
||||
const connectedIDs = new Set(connected().map((p) => p.id))
|
||||
const items = providers
|
||||
@@ -37,14 +50,53 @@ export const SettingsProviders: Component = () => {
|
||||
const current = source(item)
|
||||
if (current === "env") return language.t("settings.providers.tag.environment")
|
||||
if (current === "api") return language.t("provider.connect.method.apiKey")
|
||||
if (current === "config") return language.t("settings.providers.tag.config")
|
||||
if (current === "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")
|
||||
return language.t("settings.providers.tag.other")
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (isConfigCustom(providerID)) {
|
||||
await globalSDK.client.auth.remove({ providerID }).catch(() => undefined)
|
||||
await disableProvider(providerID, name)
|
||||
return
|
||||
}
|
||||
await globalSDK.client.auth
|
||||
.remove({ providerID })
|
||||
.then(async () => {
|
||||
@@ -63,7 +115,7 @@ export const SettingsProviders: Component = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<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]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
||||
@@ -84,14 +136,21 @@ export const SettingsProviders: Component = () => {
|
||||
>
|
||||
<For each={connected()}>
|
||||
{(item) => (
|
||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||
<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="flex items-center gap-3 min-w-0">
|
||||
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-regular text-text-strong truncate">{item.name}</span>
|
||||
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong truncate">{item.name}</span>
|
||||
<Tag>{type(item)}</Tag>
|
||||
</div>
|
||||
<Show when={canDisconnect(item)}>
|
||||
<Button size="small" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
|
||||
<Show
|
||||
when={canDisconnect(item)}
|
||||
fallback={
|
||||
<span class="text-14-regular text-text-base opacity-0 group-hover:opacity-100 transition-opacity duration-200 pr-3 cursor-default">
|
||||
Connected from your environment variables
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Button size="large" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
|
||||
{language.t("common.disconnect")}
|
||||
</Button>
|
||||
</Show>
|
||||
@@ -107,21 +166,49 @@ export const SettingsProviders: Component = () => {
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<For each={popular()}>
|
||||
{(item) => (
|
||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="flex items-center gap-x-3 min-w-0">
|
||||
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-regular text-text-strong">{item.name}</span>
|
||||
<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 flex-col min-w-0">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{item.name}</span>
|
||||
<Show when={item.id === "opencode"}>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={item.id === "opencode"}>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
<span class="text-12-regular text-text-weak pl-8">
|
||||
{language.t("dialog.provider.opencode.note")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
||||
</Show>
|
||||
<Show when={item.id === "openai"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
|
||||
<span class="text-12-regular text-text-weak pl-8">
|
||||
{language.t("dialog.provider.anthropic.note")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item.id.startsWith("github-copilot")}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
|
||||
<span class="text-12-regular text-text-weak pl-8">
|
||||
{language.t("dialog.provider.copilot.note")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item.id === "openai"}>
|
||||
<span class="text-12-regular text-text-weak pl-8">
|
||||
{language.t("dialog.provider.openai.note")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item.id === "google"}>
|
||||
<span class="text-12-regular text-text-weak pl-8">
|
||||
{language.t("dialog.provider.google.note")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item.id === "openrouter"}>
|
||||
<span class="text-12-regular text-text-weak pl-8">
|
||||
{language.t("dialog.provider.openrouter.note")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item.id === "vercel"}>
|
||||
<span class="text-12-regular text-text-weak pl-8">
|
||||
{language.t("dialog.provider.vercel.note")}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Button
|
||||
@@ -137,11 +224,32 @@ export const SettingsProviders: Component = () => {
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="px-0 py-0 text-14-medium text-text-strong text-left justify-start hover:bg-transparent active:bg-transparent"
|
||||
class="px-0 py-0 mt-5 text-14-medium text-text-interactive-base text-left justify-start hover:bg-transparent active:bg-transparent"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}}
|
||||
|
||||
@@ -15,14 +15,16 @@ import { usePlatform } from "@/context/platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { DialogSelectServer } from "./dialog-select-server"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
|
||||
type ServerStatus = { healthy: boolean; version?: string }
|
||||
|
||||
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({
|
||||
baseUrl: url,
|
||||
fetch: platform.fetch,
|
||||
signal: AbortSignal.timeout(3000),
|
||||
signal,
|
||||
})
|
||||
return sdk.global
|
||||
.health()
|
||||
@@ -39,9 +41,10 @@ export function StatusPopover() {
|
||||
const language = useLanguage()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [loading, setLoading] = createSignal<string | null>(null)
|
||||
const [store, setStore] = createStore({
|
||||
status: {} as Record<string, ServerStatus | undefined>,
|
||||
loading: null as string | null,
|
||||
defaultServerUrl: undefined as string | undefined,
|
||||
})
|
||||
|
||||
const servers = createMemo(() => {
|
||||
@@ -97,17 +100,23 @@ export function StatusPopover() {
|
||||
const mcpConnected = createMemo(() => mcpItems().filter((i) => i.status === "connected").length)
|
||||
|
||||
const toggleMcp = async (name: string) => {
|
||||
if (loading()) return
|
||||
setLoading(name)
|
||||
const status = sync.data.mcp[name]
|
||||
if (status?.status === "connected") {
|
||||
await sdk.client.mcp.disconnect({ name })
|
||||
} else {
|
||||
await sdk.client.mcp.connect({ name })
|
||||
if (store.loading) return
|
||||
setStore("loading", name)
|
||||
|
||||
try {
|
||||
const status = sync.data.mcp[name]
|
||||
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
||||
const result = await sdk.client.mcp.status()
|
||||
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)
|
||||
setLoading(null)
|
||||
}
|
||||
|
||||
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
||||
@@ -123,15 +132,21 @@ export function StatusPopover() {
|
||||
|
||||
const serverCount = createMemo(() => sortedServers().length)
|
||||
|
||||
const [defaultServerUrl, setDefaultServerUrl] = createSignal<string | undefined>()
|
||||
|
||||
createEffect(() => {
|
||||
const refreshDefaultServerUrl = () => {
|
||||
const result = platform.getDefaultServerUrl?.()
|
||||
if (result instanceof Promise) {
|
||||
result.then((url) => setDefaultServerUrl(url ? normalizeServerUrl(url) : undefined))
|
||||
if (!result) {
|
||||
setStore("defaultServerUrl", undefined)
|
||||
return
|
||||
}
|
||||
if (result) setDefaultServerUrl(normalizeServerUrl(result))
|
||||
if (result instanceof Promise) {
|
||||
result.then((url) => setStore("defaultServerUrl", url ? normalizeServerUrl(url) : undefined))
|
||||
return
|
||||
}
|
||||
setStore("defaultServerUrl", normalizeServerUrl(result))
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
refreshDefaultServerUrl()
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -161,33 +176,16 @@ export function StatusPopover() {
|
||||
placement="bottom-end"
|
||||
shift={-136}
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1 w-[360px] rounded-xl"
|
||||
style={{ "box-shadow": "var(--shadow-lg-border-base)" }}
|
||||
>
|
||||
<div class="flex items-center gap-1 w-[360px] rounded-xl shadow-[var(--shadow-lg-border-base)]">
|
||||
<Tabs
|
||||
aria-label={language.t("status.popover.ariaLabel")}
|
||||
class="tabs"
|
||||
class="tabs bg-background-strong rounded-xl overflow-hidden"
|
||||
data-component="tabs"
|
||||
data-active="servers"
|
||||
defaultValue="servers"
|
||||
variant="alt"
|
||||
style={{
|
||||
"background-color": "var(--background-strong)",
|
||||
"border-radius": "12px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Tabs.List
|
||||
data-slot="tablist"
|
||||
style={{
|
||||
"background-color": "transparent",
|
||||
"border-bottom": "none",
|
||||
padding: "8px 16px 0",
|
||||
gap: "16px",
|
||||
height: "40px",
|
||||
}}
|
||||
>
|
||||
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
||||
{serverCount() > 0 ? `${serverCount()} ` : ""}
|
||||
{language.t("status.popover.tab.servers")}
|
||||
@@ -212,7 +210,7 @@ export function StatusPopover() {
|
||||
<For each={sortedServers()}>
|
||||
{(url) => {
|
||||
const isActive = () => url === server.url
|
||||
const isDefault = () => url === defaultServerUrl()
|
||||
const isDefault = () => url === store.defaultServerUrl
|
||||
const status = () => store.status[url]
|
||||
const isBlocked = () => status()?.healthy === false
|
||||
const [truncated, setTruncated] = createSignal(false)
|
||||
@@ -294,7 +292,7 @@ export function StatusPopover() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="mt-3 self-start h-8 px-3 py-1.5"
|
||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
||||
onClick={() => dialog.show(() => <DialogSelectServer />, refreshDefaultServerUrl)}
|
||||
>
|
||||
{language.t("status.popover.action.manageServers")}
|
||||
</Button>
|
||||
@@ -321,7 +319,7 @@ export function StatusPopover() {
|
||||
type="button"
|
||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
||||
onClick={() => toggleMcp(item.name)}
|
||||
disabled={loading() === item.name}
|
||||
disabled={store.loading === item.name}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
@@ -337,7 +335,7 @@ export function StatusPopover() {
|
||||
<div onClick={(event) => event.stopPropagation()}>
|
||||
<Switch
|
||||
checked={enabled()}
|
||||
disabled={loading() === item.name}
|
||||
disabled={store.loading === item.name}
|
||||
onChange={() => toggleMcp(item.name)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { monoFontFamily, useSettings } from "@/context/settings"
|
||||
import { SerializeAddon } from "@/addons/serialize"
|
||||
import { LocalPTY } from "@/context/terminal"
|
||||
import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
|
||||
export interface TerminalProps extends ComponentProps<"div"> {
|
||||
pty: LocalPTY
|
||||
@@ -14,6 +16,19 @@ export interface TerminalProps extends ComponentProps<"div"> {
|
||||
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 = {
|
||||
background: string
|
||||
foreground: string
|
||||
@@ -40,6 +55,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
const sdk = useSDK()
|
||||
const settings = useSettings()
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
let container!: HTMLDivElement
|
||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
|
||||
let ws: WebSocket | undefined
|
||||
@@ -50,8 +66,20 @@ export const Terminal = (props: TerminalProps) => {
|
||||
let handleResize: () => void
|
||||
let handleTextareaFocus: () => void
|
||||
let handleTextareaBlur: () => void
|
||||
let reconnect: number | undefined
|
||||
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 mode = theme.mode()
|
||||
@@ -107,188 +135,237 @@ export const Terminal = (props: TerminalProps) => {
|
||||
focusTerminal()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const mod = await import("ghostty-web")
|
||||
ghostty = await mod.Ghostty.load()
|
||||
onMount(() => {
|
||||
const run = async () => {
|
||||
const loaded = await loadGhostty()
|
||||
if (disposed) return
|
||||
|
||||
const once = { value: false }
|
||||
const mod = loaded.mod
|
||||
const g = loaded.ghostty
|
||||
|
||||
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 once = { value: false }
|
||||
|
||||
const t = new mod.Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: "bar",
|
||||
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 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 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
|
||||
})
|
||||
|
||||
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()
|
||||
const socket = new WebSocket(url)
|
||||
cleanups.push(() => {
|
||||
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
|
||||
})
|
||||
}
|
||||
if (disposed) {
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
ws = socket
|
||||
|
||||
fitAddon.observeResize()
|
||||
handleResize = () => fitAddon.fit()
|
||||
window.addEventListener("resize", handleResize)
|
||||
t.onResize(async (size) => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
await sdk.client.pty
|
||||
const t = new mod.Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: "bar",
|
||||
fontSize: 14,
|
||||
fontFamily: monoFontFamily(settings.appearance.font()),
|
||||
allowTransparency: true,
|
||||
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({
|
||||
ptyID: local.pty.id,
|
||||
size: {
|
||||
cols: size.cols,
|
||||
rows: size.rows,
|
||||
cols: t.cols,
|
||||
rows: t.rows,
|
||||
},
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
})
|
||||
t.onData((data) => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(data)
|
||||
socket.addEventListener("open", handleOpen)
|
||||
cleanups.push(() => socket.removeEventListener("open", handleOpen))
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
t.write(event.data)
|
||||
}
|
||||
})
|
||||
t.onKey((key) => {
|
||||
if (key.key == "Enter") {
|
||||
props.onSubmit?.()
|
||||
}
|
||||
})
|
||||
// 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) {
|
||||
socket.addEventListener("message", handleMessage)
|
||||
cleanups.push(() => socket.removeEventListener("message", handleMessage))
|
||||
|
||||
const handleError = (error: Event) => {
|
||||
if (disposed) return
|
||||
if (once.value) return
|
||||
once.value = true
|
||||
local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
|
||||
console.error("WebSocket error:", error)
|
||||
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(() => {
|
||||
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
|
||||
if (serializeAddon && props.onCleanup && t) {
|
||||
const buffer = serializeAddon.serialize()
|
||||
const buffer = (() => {
|
||||
try {
|
||||
return serializeAddon.serialize()
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
})()
|
||||
props.onCleanup({
|
||||
...local.pty,
|
||||
buffer,
|
||||
@@ -298,8 +375,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
})
|
||||
}
|
||||
|
||||
ws?.close()
|
||||
t?.dispose()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createEffect, createMemo, Show } from "solid-js"
|
||||
import { createEffect, createMemo, Show, untrack } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLocation, useNavigate } from "@solidjs/router"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { useTheme } from "@opencode-ai/ui/theme"
|
||||
|
||||
import { useLayout } from "@/context/layout"
|
||||
@@ -16,11 +18,68 @@ export function Titlebar() {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const theme = useTheme()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||
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 = () => {
|
||||
if (platform.platform !== "desktop") return
|
||||
|
||||
@@ -106,34 +165,58 @@ export function Titlebar() {
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||
placement="bottom"
|
||||
title={language.t("command.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()}
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<TooltipKeybind
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||
placement="bottom"
|
||||
title={language.t("command.sidebar.toggle")}
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
>
|
||||
<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"
|
||||
<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()}
|
||||
>
|
||||
<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")}
|
||||
/>
|
||||
<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"
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="arrow-right"
|
||||
class="size-6 p-0"
|
||||
disabled={!canForward()}
|
||||
onClick={forward}
|
||||
aria-label={language.t("common.goForward")}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
@@ -24,6 +24,15 @@ function normalizeKey(key: string) {
|
||||
return key.toLowerCase()
|
||||
}
|
||||
|
||||
function signature(key: string, ctrl: boolean, meta: boolean, shift: boolean, alt: boolean) {
|
||||
const mask = (ctrl ? 1 : 0) | (meta ? 2 : 0) | (shift ? 4 : 0) | (alt ? 8 : 0)
|
||||
return `${key}:${mask}`
|
||||
}
|
||||
|
||||
function signatureFromEvent(event: KeyboardEvent) {
|
||||
return signature(normalizeKey(event.key), event.ctrlKey, event.metaKey, event.shiftKey, event.altKey)
|
||||
}
|
||||
|
||||
export type KeybindConfig = string
|
||||
|
||||
export interface Keybind {
|
||||
@@ -156,8 +165,10 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
const dialog = useDialog()
|
||||
const settings = useSettings()
|
||||
const language = useLanguage()
|
||||
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
||||
const [store, setStore] = createStore({
|
||||
registrations: [] as Accessor<CommandOption[]>[],
|
||||
suspendCount: 0,
|
||||
})
|
||||
|
||||
const [catalog, setCatalog, _, catalogReady] = persisted(
|
||||
Persist.global("command.catalog.v1"),
|
||||
@@ -175,7 +186,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
const seen = new Set<string>()
|
||||
const all: CommandOption[] = []
|
||||
|
||||
for (const reg of registrations()) {
|
||||
for (const reg of store.registrations) {
|
||||
for (const opt of reg()) {
|
||||
if (seen.has(opt.id)) continue
|
||||
seen.add(opt.id)
|
||||
@@ -221,7 +232,31 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
]
|
||||
})
|
||||
|
||||
const suspended = () => suspendCount() > 0
|
||||
const suspended = () => store.suspendCount > 0
|
||||
|
||||
const palette = createMemo(() => {
|
||||
const config = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
|
||||
const keybinds = parseKeybind(config)
|
||||
return new Set(keybinds.map((kb) => signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)))
|
||||
})
|
||||
|
||||
const keymap = createMemo(() => {
|
||||
const map = new Map<string, CommandOption>()
|
||||
for (const option of options()) {
|
||||
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
|
||||
if (option.disabled) continue
|
||||
if (!option.keybind) continue
|
||||
|
||||
const keybinds = parseKeybind(option.keybind)
|
||||
for (const kb of keybinds) {
|
||||
if (!kb.key) continue
|
||||
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
|
||||
if (map.has(sig)) continue
|
||||
map.set(sig, option)
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
|
||||
for (const option of options()) {
|
||||
@@ -239,24 +274,18 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (suspended() || dialog.active) return
|
||||
|
||||
const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
|
||||
if (matchKeybind(paletteKeybinds, event)) {
|
||||
const sig = signatureFromEvent(event)
|
||||
|
||||
if (palette().has(sig)) {
|
||||
event.preventDefault()
|
||||
showPalette()
|
||||
return
|
||||
}
|
||||
|
||||
for (const option of options()) {
|
||||
if (option.disabled) continue
|
||||
if (!option.keybind) continue
|
||||
|
||||
const keybinds = parseKeybind(option.keybind)
|
||||
if (matchKeybind(keybinds, event)) {
|
||||
event.preventDefault()
|
||||
option.onSelect?.("keybind")
|
||||
return
|
||||
}
|
||||
}
|
||||
const option = keymap().get(sig)
|
||||
if (!option) return
|
||||
event.preventDefault()
|
||||
option.onSelect?.("keybind")
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -270,9 +299,9 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
return {
|
||||
register(cb: () => CommandOption[]) {
|
||||
const results = createMemo(cb)
|
||||
setRegistrations((arr) => [results, ...arr])
|
||||
setStore("registrations", (arr) => [results, ...arr])
|
||||
onCleanup(() => {
|
||||
setRegistrations((arr) => arr.filter((x) => x !== results))
|
||||
setStore("registrations", (arr) => arr.filter((x) => x !== results))
|
||||
})
|
||||
},
|
||||
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
||||
@@ -294,7 +323,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
},
|
||||
show: showPalette,
|
||||
keybinds(enabled: boolean) {
|
||||
setSuspendCount((count) => count + (enabled ? -1 : 1))
|
||||
setStore("suspendCount", (count) => count + (enabled ? -1 : 1))
|
||||
},
|
||||
suspended,
|
||||
get catalog() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js"
|
||||
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useParams } from "@solidjs/router"
|
||||
@@ -37,8 +37,16 @@ function createCommentSession(dir: string, id: string | undefined) {
|
||||
}),
|
||||
)
|
||||
|
||||
const [focus, setFocus] = createSignal<CommentFocus | null>(null)
|
||||
const [active, setActive] = createSignal<CommentFocus | null>(null)
|
||||
const [state, setState] = createStore({
|
||||
focus: null as CommentFocus | null,
|
||||
active: null as CommentFocus | null,
|
||||
})
|
||||
|
||||
const setFocus = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
|
||||
setState("focus", value)
|
||||
|
||||
const setActive = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
|
||||
setState("active", value)
|
||||
|
||||
const list = (file: string) => store.comments[file] ?? []
|
||||
|
||||
@@ -74,10 +82,10 @@ function createCommentSession(dir: string, id: string | undefined) {
|
||||
all,
|
||||
add,
|
||||
remove,
|
||||
focus: createMemo(() => focus()),
|
||||
focus: createMemo(() => state.focus),
|
||||
setFocus,
|
||||
clearFocus: () => setFocus(null),
|
||||
active: createMemo(() => active()),
|
||||
active: createMemo(() => state.active),
|
||||
setActive,
|
||||
clearActive: () => setActive(null),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import type { FileContent } from "@opencode-ai/sdk/v2"
|
||||
import type { FileContent, FileNode } from "@opencode-ai/sdk/v2"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
@@ -39,6 +39,14 @@ export type FileState = {
|
||||
content?: FileContent
|
||||
}
|
||||
|
||||
type DirectoryState = {
|
||||
expanded: boolean
|
||||
loaded?: boolean
|
||||
loading?: boolean
|
||||
error?: string
|
||||
children?: string[]
|
||||
}
|
||||
|
||||
function stripFileProtocol(input: string) {
|
||||
if (!input.startsWith("file://")) return input
|
||||
return input.slice("file://".length)
|
||||
@@ -57,6 +65,62 @@ function stripQueryAndHash(input: string) {
|
||||
return input
|
||||
}
|
||||
|
||||
function unquoteGitPath(input: string) {
|
||||
if (!input.startsWith('"')) return input
|
||||
if (!input.endsWith('"')) return input
|
||||
const body = input.slice(1, -1)
|
||||
const bytes: number[] = []
|
||||
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
const char = body[i]!
|
||||
if (char !== "\\") {
|
||||
bytes.push(char.charCodeAt(0))
|
||||
continue
|
||||
}
|
||||
|
||||
const next = body[i + 1]
|
||||
if (!next) {
|
||||
bytes.push("\\".charCodeAt(0))
|
||||
continue
|
||||
}
|
||||
|
||||
if (next >= "0" && next <= "7") {
|
||||
const chunk = body.slice(i + 1, i + 4)
|
||||
const match = chunk.match(/^[0-7]{1,3}/)
|
||||
if (!match) {
|
||||
bytes.push(next.charCodeAt(0))
|
||||
i++
|
||||
continue
|
||||
}
|
||||
bytes.push(parseInt(match[0], 8))
|
||||
i += match[0].length
|
||||
continue
|
||||
}
|
||||
|
||||
const escaped =
|
||||
next === "n"
|
||||
? "\n"
|
||||
: next === "r"
|
||||
? "\r"
|
||||
: next === "t"
|
||||
? "\t"
|
||||
: next === "b"
|
||||
? "\b"
|
||||
: next === "f"
|
||||
? "\f"
|
||||
: next === "v"
|
||||
? "\v"
|
||||
: next === "\\" || next === '"'
|
||||
? next
|
||||
: undefined
|
||||
|
||||
bytes.push((escaped ?? next).charCodeAt(0))
|
||||
i++
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(bytes))
|
||||
}
|
||||
|
||||
export function selectionFromLines(range: SelectedLineRange): FileSelection {
|
||||
const startLine = Math.min(range.start, range.end)
|
||||
const endLine = Math.max(range.start, range.end)
|
||||
@@ -87,6 +151,28 @@ const WORKSPACE_KEY = "__workspace__"
|
||||
const MAX_FILE_VIEW_SESSIONS = 20
|
||||
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 ViewCacheEntry = {
|
||||
@@ -197,7 +283,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
const root = directory()
|
||||
const prefix = root.endsWith("/") ? root : root + "/"
|
||||
|
||||
let path = stripQueryAndHash(stripFileProtocol(input))
|
||||
let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input)))
|
||||
|
||||
if (path.startsWith(prefix)) {
|
||||
path = path.slice(prefix.length)
|
||||
@@ -229,6 +315,13 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
}
|
||||
|
||||
const inflight = 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<{
|
||||
file: Record<string, FileState>
|
||||
@@ -236,10 +329,51 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
file: {},
|
||||
})
|
||||
|
||||
const [tree, setTree] = createStore<{
|
||||
node: Record<string, FileNode>
|
||||
dir: Record<string, DirectoryState>
|
||||
}>({
|
||||
node: {},
|
||||
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(() => {
|
||||
scope()
|
||||
inflight.clear()
|
||||
treeInflight.clear()
|
||||
contentLru.clear()
|
||||
setStore("file", {})
|
||||
setTree("node", {})
|
||||
setTree("dir", { "": { expanded: true } })
|
||||
})
|
||||
|
||||
const viewCache = new Map<string, ViewCacheEntry>()
|
||||
@@ -317,15 +451,20 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
.read({ path })
|
||||
.then((x) => {
|
||||
if (scope() !== directory) return
|
||||
const content = x.data
|
||||
setStore(
|
||||
"file",
|
||||
path,
|
||||
produce((draft) => {
|
||||
draft.loaded = true
|
||||
draft.loading = false
|
||||
draft.content = x.data
|
||||
draft.content = content
|
||||
}),
|
||||
)
|
||||
|
||||
if (!content) return
|
||||
touchContent(path, approxBytes(content))
|
||||
evictContent(new Set([path]))
|
||||
})
|
||||
.catch((e) => {
|
||||
if (scope() !== directory) return
|
||||
@@ -351,17 +490,182 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
return promise
|
||||
}
|
||||
|
||||
function normalizeDir(input: string) {
|
||||
return normalize(input).replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
function ensureDir(path: string) {
|
||||
if (tree.dir[path]) return
|
||||
setTree("dir", path, { expanded: false })
|
||||
}
|
||||
|
||||
function listDir(input: string, options?: { force?: boolean }) {
|
||||
const dir = normalizeDir(input)
|
||||
ensureDir(dir)
|
||||
|
||||
const current = tree.dir[dir]
|
||||
if (!options?.force && current?.loaded) return Promise.resolve()
|
||||
|
||||
const pending = treeInflight.get(dir)
|
||||
if (pending) return pending
|
||||
|
||||
setTree(
|
||||
"dir",
|
||||
dir,
|
||||
produce((draft) => {
|
||||
draft.loading = true
|
||||
draft.error = undefined
|
||||
}),
|
||||
)
|
||||
|
||||
const directory = scope()
|
||||
|
||||
const promise = sdk.client.file
|
||||
.list({ path: dir })
|
||||
.then((x) => {
|
||||
if (scope() !== directory) return
|
||||
const nodes = x.data ?? []
|
||||
const prevChildren = tree.dir[dir]?.children ?? []
|
||||
const nextChildren = nodes.map((node) => node.path)
|
||||
const nextSet = new Set(nextChildren)
|
||||
|
||||
setTree(
|
||||
"node",
|
||||
produce((draft) => {
|
||||
const removedDirs: string[] = []
|
||||
|
||||
for (const child of prevChildren) {
|
||||
if (nextSet.has(child)) continue
|
||||
const existing = draft[child]
|
||||
if (existing?.type === "directory") removedDirs.push(child)
|
||||
delete draft[child]
|
||||
}
|
||||
|
||||
if (removedDirs.length > 0) {
|
||||
const keys = Object.keys(draft)
|
||||
for (const key of keys) {
|
||||
for (const removed of removedDirs) {
|
||||
if (!key.startsWith(removed + "/")) continue
|
||||
delete draft[key]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
draft[node.path] = node
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
setTree(
|
||||
"dir",
|
||||
dir,
|
||||
produce((draft) => {
|
||||
draft.loaded = true
|
||||
draft.loading = false
|
||||
draft.children = nextChildren
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
if (scope() !== directory) return
|
||||
setTree(
|
||||
"dir",
|
||||
dir,
|
||||
produce((draft) => {
|
||||
draft.loading = false
|
||||
draft.error = e.message
|
||||
}),
|
||||
)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.file.listFailed.title"),
|
||||
description: e.message,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
treeInflight.delete(dir)
|
||||
})
|
||||
|
||||
treeInflight.set(dir, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
function expandDir(input: string) {
|
||||
const dir = normalizeDir(input)
|
||||
ensureDir(dir)
|
||||
setTree("dir", dir, "expanded", true)
|
||||
void listDir(dir)
|
||||
}
|
||||
|
||||
function collapseDir(input: string) {
|
||||
const dir = normalizeDir(input)
|
||||
ensureDir(dir)
|
||||
setTree("dir", dir, "expanded", false)
|
||||
}
|
||||
|
||||
function dirState(input: string) {
|
||||
const dir = normalizeDir(input)
|
||||
return tree.dir[dir]
|
||||
}
|
||||
|
||||
function children(input: string) {
|
||||
const dir = normalizeDir(input)
|
||||
const ids = tree.dir[dir]?.children
|
||||
if (!ids) return []
|
||||
const out: FileNode[] = []
|
||||
for (const id of ids) {
|
||||
const node = tree.node[id]
|
||||
if (node) out.push(node)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const stop = sdk.event.listen((e) => {
|
||||
const event = e.details
|
||||
if (event.type !== "file.watcher.updated") return
|
||||
const path = normalize(event.properties.file)
|
||||
if (!path) return
|
||||
if (path.startsWith(".git/")) return
|
||||
if (!store.file[path]) return
|
||||
load(path, { force: true })
|
||||
|
||||
if (store.file[path]) {
|
||||
load(path, { force: true })
|
||||
}
|
||||
|
||||
const kind = event.properties.event
|
||||
if (kind === "change") {
|
||||
const dir = (() => {
|
||||
if (path === "") return ""
|
||||
const node = tree.node[path]
|
||||
if (node?.type !== "directory") return
|
||||
return path
|
||||
})()
|
||||
if (dir === undefined) return
|
||||
if (!tree.dir[dir]?.loaded) return
|
||||
listDir(dir, { force: true })
|
||||
return
|
||||
}
|
||||
if (kind !== "add" && kind !== "unlink") return
|
||||
|
||||
const parent = path.split("/").slice(0, -1).join("/")
|
||||
if (!tree.dir[parent]?.loaded) return
|
||||
|
||||
listDir(parent, { force: true })
|
||||
})
|
||||
|
||||
const get = (input: string) => store.file[normalize(input)]
|
||||
const get = (input: string) => {
|
||||
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 scrollLeft = (input: string) => view().scrollLeft(normalize(input))
|
||||
@@ -392,6 +696,21 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
normalize,
|
||||
tab,
|
||||
pathFromTab,
|
||||
tree: {
|
||||
list: listDir,
|
||||
refresh: (input: string) => listDir(input, { force: true }),
|
||||
state: dirState,
|
||||
children,
|
||||
expand: expandDir,
|
||||
collapse: collapseDir,
|
||||
toggle(input: string) {
|
||||
if (dirState(input)?.expanded) {
|
||||
collapseDir(input)
|
||||
return
|
||||
}
|
||||
expandDir(input)
|
||||
},
|
||||
},
|
||||
get,
|
||||
load,
|
||||
scrollTop,
|
||||
@@ -400,10 +719,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
setScrollLeft,
|
||||
selectedLines,
|
||||
setSelectedLines,
|
||||
searchFiles: (query: string) =>
|
||||
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)),
|
||||
searchFiles: (query: string) => search(query, "false"),
|
||||
searchFilesAndDirectories: (query: string) => search(query, "true"),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ import { createStore, produce, reconcile, type SetStoreFunction, type Store } fr
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { ErrorPage, type InitError } from "../pages/error"
|
||||
import type { InitError } from "../pages/error"
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
@@ -188,7 +188,74 @@ function createGlobalSync() {
|
||||
config: {},
|
||||
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(() => {
|
||||
if (!projectCacheReady()) return
|
||||
@@ -210,14 +277,8 @@ function createGlobalSync() {
|
||||
|
||||
createEffect(() => {
|
||||
if (globalStore.reload !== "complete") return
|
||||
if (bootstrapQueue.length) {
|
||||
for (const directory of bootstrapQueue) {
|
||||
bootstrapInstance(directory)
|
||||
}
|
||||
bootstrap()
|
||||
}
|
||||
bootstrapQueue = []
|
||||
setGlobalStore("reload", undefined)
|
||||
refresh()
|
||||
})
|
||||
|
||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||
@@ -225,6 +286,65 @@ function createGlobalSync() {
|
||||
const sessionLoads = new Map<string, Promise<void>>()
|
||||
const sessionMeta = new Map<string, { limit: number }>()
|
||||
|
||||
const sessionRecentWindow = 4 * 60 * 60 * 1000
|
||||
const sessionRecentLimit = 50
|
||||
|
||||
function sessionUpdatedAt(session: Session) {
|
||||
return session.time.updated ?? session.time.created
|
||||
}
|
||||
|
||||
function compareSessionRecent(a: Session, b: Session) {
|
||||
const aUpdated = sessionUpdatedAt(a)
|
||||
const bUpdated = sessionUpdatedAt(b)
|
||||
if (aUpdated !== bUpdated) return bUpdated - aUpdated
|
||||
return a.id.localeCompare(b.id)
|
||||
}
|
||||
|
||||
function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) {
|
||||
if (limit <= 0) return [] as Session[]
|
||||
const selected: Session[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const session of sessions) {
|
||||
if (!session?.id) continue
|
||||
if (seen.has(session.id)) continue
|
||||
seen.add(session.id)
|
||||
|
||||
if (sessionUpdatedAt(session) <= cutoff) continue
|
||||
|
||||
const index = selected.findIndex((x) => compareSessionRecent(session, x) < 0)
|
||||
if (index === -1) selected.push(session)
|
||||
if (index !== -1) selected.splice(index, 0, session)
|
||||
if (selected.length > limit) selected.pop()
|
||||
}
|
||||
return selected
|
||||
}
|
||||
|
||||
function trimSessions(input: Session[], options: { limit: number; permission: Record<string, PermissionRequest[]> }) {
|
||||
const limit = Math.max(0, options.limit)
|
||||
const cutoff = Date.now() - sessionRecentWindow
|
||||
const all = input
|
||||
.filter((s) => !!s?.id)
|
||||
.filter((s) => !s.time?.archived)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
const roots = all.filter((s) => !s.parentID)
|
||||
const children = all.filter((s) => !!s.parentID)
|
||||
|
||||
const base = roots.slice(0, limit)
|
||||
const recent = takeRecentSessions(roots.slice(limit), sessionRecentLimit, cutoff)
|
||||
const keepRoots = [...base, ...recent]
|
||||
|
||||
const keepRootIds = new Set(keepRoots.map((s) => s.id))
|
||||
const keepChildren = children.filter((s) => {
|
||||
if (s.parentID && keepRootIds.has(s.parentID)) return true
|
||||
const perms = options.permission[s.id] ?? []
|
||||
if (perms.length > 0) return true
|
||||
return sessionUpdatedAt(s) > cutoff
|
||||
})
|
||||
|
||||
return [...keepRoots, ...keepChildren].sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
|
||||
function ensureChild(directory: string) {
|
||||
if (!directory) console.error("No directory provided")
|
||||
if (!children[directory]) {
|
||||
@@ -323,7 +443,13 @@ function createGlobalSync() {
|
||||
|
||||
const [store, setStore] = child(directory, { bootstrap: false })
|
||||
const meta = sessionMeta.get(directory)
|
||||
if (meta && meta.limit >= store.limit) return
|
||||
if (meta && meta.limit >= store.limit) {
|
||||
const next = trimSessions(store.session, { limit: store.limit, permission: store.permission })
|
||||
if (next.length !== store.session.length) {
|
||||
setStore("session", reconcile(next, { key: "id" }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const promise = globalSDK.client.session
|
||||
.list({ directory, roots: true })
|
||||
@@ -337,21 +463,9 @@ function createGlobalSync() {
|
||||
// a request is in-flight still get the expanded result.
|
||||
const limit = store.limit
|
||||
|
||||
const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
|
||||
if (sandboxWorkspace) {
|
||||
setStore("sessionTotal", nonArchived.length)
|
||||
setStore("session", reconcile(nonArchived, { key: "id" }))
|
||||
sessionMeta.set(directory, { limit })
|
||||
return
|
||||
}
|
||||
const children = store.session.filter((s) => !!s.parentID)
|
||||
const sessions = trimSessions([...nonArchived, ...children], { limit, permission: store.permission })
|
||||
|
||||
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
||||
// Include up to the limit, plus any updated in the last 4 hours
|
||||
const sessions = nonArchived.filter((s, i) => {
|
||||
if (i < limit) return true
|
||||
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
|
||||
return updated > fourHoursAgo
|
||||
})
|
||||
// Store total session count (used for "load more" pagination)
|
||||
setStore("sessionTotal", nonArchived.length)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
@@ -493,6 +607,37 @@ function createGlobalSync() {
|
||||
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 directory = e.name
|
||||
const event = e.details
|
||||
@@ -500,9 +645,8 @@ function createGlobalSync() {
|
||||
if (directory === "global") {
|
||||
switch (event?.type) {
|
||||
case "global.disposed": {
|
||||
if (globalStore.reload) return
|
||||
bootstrap()
|
||||
break
|
||||
refresh()
|
||||
return
|
||||
}
|
||||
case "project.updated": {
|
||||
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
|
||||
@@ -526,35 +670,66 @@ function createGlobalSync() {
|
||||
if (!existing) return
|
||||
|
||||
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) {
|
||||
case "server.instance.disposed": {
|
||||
if (globalStore.reload) {
|
||||
bootstrapQueue.push(directory)
|
||||
return
|
||||
}
|
||||
bootstrapInstance(directory)
|
||||
break
|
||||
push(directory)
|
||||
return
|
||||
}
|
||||
case "session.created": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
const info = event.properties.info
|
||||
const result = Binary.search(store.session, info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
setStore("session", result.index, reconcile(info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
if (!event.properties.info.parentID) {
|
||||
setStore("sessionTotal", store.sessionTotal + 1)
|
||||
const next = store.session.slice()
|
||||
next.splice(result.index, 0, info)
|
||||
const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission })
|
||||
setStore("session", reconcile(trimmed, { key: "id" }))
|
||||
if (!info.parentID) {
|
||||
setStore("sessionTotal", (value) => value + 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "session.updated": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (event.properties.info.time.archived) {
|
||||
const info = event.properties.info
|
||||
const result = Binary.search(store.session, info.id, (s) => s.id)
|
||||
if (info.time.archived) {
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"session",
|
||||
@@ -563,24 +738,24 @@ function createGlobalSync() {
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (event.properties.info.parentID) break
|
||||
cleanupSessionCaches(info.id)
|
||||
if (info.parentID) break
|
||||
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
break
|
||||
}
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
setStore("session", result.index, reconcile(info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
const next = store.session.slice()
|
||||
next.splice(result.index, 0, info)
|
||||
const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission })
|
||||
setStore("session", reconcile(trimmed, { key: "id" }))
|
||||
break
|
||||
}
|
||||
case "session.deleted": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
const sessionID = event.properties.info.id
|
||||
const result = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"session",
|
||||
@@ -589,6 +764,7 @@ function createGlobalSync() {
|
||||
}),
|
||||
)
|
||||
}
|
||||
cleanupSessionCaches(sessionID)
|
||||
if (event.properties.info.parentID) break
|
||||
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
break
|
||||
@@ -624,18 +800,22 @@ function createGlobalSync() {
|
||||
break
|
||||
}
|
||||
case "message.removed": {
|
||||
const messages = store.message[event.properties.sessionID]
|
||||
if (!messages) break
|
||||
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"message",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
const sessionID = event.properties.sessionID
|
||||
const messageID = event.properties.messageID
|
||||
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[sessionID]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
if (result.found) {
|
||||
messages.splice(result.index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
delete draft.part[messageID]
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "message.part.updated": {
|
||||
@@ -660,15 +840,19 @@ function createGlobalSync() {
|
||||
break
|
||||
}
|
||||
case "message.part.removed": {
|
||||
const parts = store.part[event.properties.messageID]
|
||||
const messageID = event.properties.messageID
|
||||
const parts = store.part[messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.messageID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
const list = draft.part[messageID]
|
||||
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]
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -765,6 +949,10 @@ function createGlobalSync() {
|
||||
}
|
||||
})
|
||||
onCleanup(unsub)
|
||||
onCleanup(() => {
|
||||
if (!timer) return
|
||||
clearTimeout(timer)
|
||||
})
|
||||
|
||||
async function bootstrap() {
|
||||
const health = await globalSDK.client.global
|
||||
@@ -772,18 +960,23 @@ function createGlobalSync() {
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
if (!health?.healthy) {
|
||||
setGlobalStore("error", new Error(language.t("error.globalSync.connectFailed", { url: globalSDK.url })))
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("dialog.server.add.error"),
|
||||
description: language.t("error.globalSync.connectFailed", { url: globalSDK.url }),
|
||||
})
|
||||
setGlobalStore("ready", true)
|
||||
return
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
const tasks = [
|
||||
retry(() =>
|
||||
globalSDK.client.path.get().then((x) => {
|
||||
setGlobalStore("path", x.data!)
|
||||
}),
|
||||
),
|
||||
retry(() =>
|
||||
globalSDK.client.config.get().then((x) => {
|
||||
globalSDK.client.global.config.get().then((x) => {
|
||||
setGlobalStore("config", x.data!)
|
||||
}),
|
||||
),
|
||||
@@ -807,9 +1000,22 @@ function createGlobalSync() {
|
||||
setGlobalStore("provider_auth", x.data ?? {})
|
||||
}),
|
||||
),
|
||||
])
|
||||
.then(() => setGlobalStore("ready", true))
|
||||
.catch((e) => setGlobalStore("error", e))
|
||||
]
|
||||
|
||||
const results = await Promise.allSettled(tasks)
|
||||
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(() => {
|
||||
@@ -853,13 +1059,13 @@ function createGlobalSync() {
|
||||
},
|
||||
child,
|
||||
bootstrap,
|
||||
updateConfig: async (config: Config) => {
|
||||
updateConfig: (config: Config) => {
|
||||
setGlobalStore("reload", "pending")
|
||||
const response = await globalSDK.client.config.update({ config })
|
||||
setTimeout(() => {
|
||||
setGlobalStore("reload", "complete")
|
||||
}, 1000)
|
||||
return response
|
||||
return globalSDK.client.global.config.update({ config }).finally(() => {
|
||||
setTimeout(() => {
|
||||
setGlobalStore("reload", "complete")
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
project: {
|
||||
loadSessions,
|
||||
@@ -875,9 +1081,6 @@ export function GlobalSyncProvider(props: ParentProps) {
|
||||
const value = createGlobalSync()
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={value.error}>
|
||||
<ErrorPage error={value.error} />
|
||||
</Match>
|
||||
<Match when={value.ready}>
|
||||
<GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
|
||||
</Match>
|
||||
|
||||
225
packages/app/src/context/highlights.tsx
Normal file
225
packages/app/src/context/highlights.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { persisted } from "@/utils/persist"
|
||||
import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes"
|
||||
|
||||
const CHANGELOG_URL = "https://opencode.ai/changelog.json"
|
||||
|
||||
type Store = {
|
||||
version?: string
|
||||
}
|
||||
|
||||
type ParsedRelease = {
|
||||
tag?: string
|
||||
highlights: Highlight[]
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function getText(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
const text = value.trim()
|
||||
return text.length > 0 ? text : undefined
|
||||
}
|
||||
|
||||
if (typeof value === "number") return String(value)
|
||||
return
|
||||
}
|
||||
|
||||
function normalizeVersion(value: string | undefined) {
|
||||
const text = value?.trim()
|
||||
if (!text) return
|
||||
return text.startsWith("v") || text.startsWith("V") ? text.slice(1) : text
|
||||
}
|
||||
|
||||
function parseMedia(value: unknown, alt: string): Highlight["media"] | undefined {
|
||||
if (!isRecord(value)) return
|
||||
const type = getText(value.type)?.toLowerCase()
|
||||
const src = getText(value.src) ?? getText(value.url)
|
||||
if (!src) return
|
||||
if (type !== "image" && type !== "video") return
|
||||
|
||||
return { type, src, alt }
|
||||
}
|
||||
|
||||
function parseHighlight(value: unknown): Highlight | undefined {
|
||||
if (!isRecord(value)) return
|
||||
|
||||
const title = getText(value.title)
|
||||
if (!title) return
|
||||
|
||||
const description = getText(value.description) ?? getText(value.shortDescription)
|
||||
if (!description) return
|
||||
|
||||
const media = parseMedia(value.media, title)
|
||||
return { title, description, media }
|
||||
}
|
||||
|
||||
function parseRelease(value: unknown): ParsedRelease | undefined {
|
||||
if (!isRecord(value)) return
|
||||
const tag = getText(value.tag) ?? getText(value.tag_name) ?? getText(value.name)
|
||||
|
||||
if (!Array.isArray(value.highlights)) {
|
||||
return { tag, highlights: [] }
|
||||
}
|
||||
|
||||
const highlights = value.highlights.flatMap((group) => {
|
||||
if (!isRecord(group)) return []
|
||||
|
||||
const source = getText(group.source)
|
||||
if (!source) return []
|
||||
if (!source.toLowerCase().includes("desktop")) return []
|
||||
|
||||
if (Array.isArray(group.items)) {
|
||||
return group.items.map((item) => parseHighlight(item)).filter((item): item is Highlight => item !== undefined)
|
||||
}
|
||||
|
||||
const item = parseHighlight(group)
|
||||
if (!item) return []
|
||||
return [item]
|
||||
})
|
||||
|
||||
return { tag, highlights }
|
||||
}
|
||||
|
||||
function parseChangelog(value: unknown): ParsedRelease[] | undefined {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
|
||||
}
|
||||
|
||||
if (!isRecord(value)) return
|
||||
if (!Array.isArray(value.releases)) return
|
||||
|
||||
return value.releases.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
|
||||
}
|
||||
|
||||
function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; previous?: string }) {
|
||||
const current = normalizeVersion(input.current)
|
||||
const previous = normalizeVersion(input.previous)
|
||||
const releases = input.releases
|
||||
|
||||
const start = (() => {
|
||||
if (!current) return 0
|
||||
const index = releases.findIndex((release) => normalizeVersion(release.tag) === current)
|
||||
return index === -1 ? 0 : index
|
||||
})()
|
||||
|
||||
const end = (() => {
|
||||
if (!previous) return releases.length
|
||||
const index = releases.findIndex((release, i) => i >= start && normalizeVersion(release.tag) === previous)
|
||||
return index === -1 ? releases.length : index
|
||||
})()
|
||||
|
||||
const highlights = releases.slice(start, end).flatMap((release) => release.highlights)
|
||||
const seen = new Set<string>()
|
||||
const unique = highlights.filter((highlight) => {
|
||||
const key = [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join(
|
||||
"\n",
|
||||
)
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
return unique.slice(0, 5)
|
||||
}
|
||||
|
||||
export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({
|
||||
name: "Highlights",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
const settings = useSettings()
|
||||
const [store, setStore, _, ready] = persisted("highlights.v1", createStore<Store>({ version: undefined }))
|
||||
|
||||
const [from, setFrom] = createSignal<string | undefined>(undefined)
|
||||
const [to, setTo] = createSignal<string | undefined>(undefined)
|
||||
const [timer, setTimer] = createSignal<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
const state = { started: false }
|
||||
|
||||
const markSeen = () => {
|
||||
if (!platform.version) return
|
||||
setStore("version", platform.version)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (state.started) return
|
||||
if (!ready()) return
|
||||
if (!settings.ready()) return
|
||||
if (!platform.version) return
|
||||
state.started = true
|
||||
|
||||
const previous = store.version
|
||||
if (!previous) {
|
||||
setStore("version", platform.version)
|
||||
return
|
||||
}
|
||||
|
||||
if (previous === platform.version) return
|
||||
|
||||
setFrom(previous)
|
||||
setTo(platform.version)
|
||||
|
||||
if (!settings.general.releaseNotes()) {
|
||||
markSeen()
|
||||
return
|
||||
}
|
||||
|
||||
const fetcher = platform.fetch ?? fetch
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => {
|
||||
controller.abort()
|
||||
const id = timer()
|
||||
if (id === undefined) return
|
||||
clearTimeout(id)
|
||||
})
|
||||
|
||||
fetcher(CHANGELOG_URL, {
|
||||
signal: controller.signal,
|
||||
headers: { Accept: "application/json" },
|
||||
})
|
||||
.then((response) => (response.ok ? (response.json() as Promise<unknown>) : undefined))
|
||||
.then((json) => {
|
||||
if (!json) return
|
||||
const releases = parseChangelog(json)
|
||||
if (!releases) return
|
||||
if (releases.length === 0) return
|
||||
const highlights = sliceHighlights({
|
||||
releases,
|
||||
current: platform.version,
|
||||
previous,
|
||||
})
|
||||
|
||||
if (controller.signal.aborted) return
|
||||
|
||||
if (highlights.length === 0) {
|
||||
markSeen()
|
||||
return
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
markSeen()
|
||||
dialog.show(() => <DialogReleaseNotes highlights={highlights} />)
|
||||
}, 500)
|
||||
setTimer(timer)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
})
|
||||
|
||||
return {
|
||||
ready,
|
||||
from,
|
||||
to,
|
||||
get last() {
|
||||
return store.version
|
||||
},
|
||||
markSeen,
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -17,6 +17,7 @@ import { dict as ru } from "@/i18n/ru"
|
||||
import { dict as ar } from "@/i18n/ar"
|
||||
import { dict as no } from "@/i18n/no"
|
||||
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 uiZh } from "@opencode-ai/ui/i18n/zh"
|
||||
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
|
||||
@@ -31,13 +32,45 @@ import { dict as uiRu } from "@opencode-ai/ui/i18n/ru"
|
||||
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
|
||||
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
|
||||
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
|
||||
import { dict as uiTh } from "@opencode-ai/ui/i18n/th"
|
||||
|
||||
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
|
||||
export type Locale =
|
||||
| "en"
|
||||
| "zh"
|
||||
| "zht"
|
||||
| "ko"
|
||||
| "de"
|
||||
| "es"
|
||||
| "fr"
|
||||
| "da"
|
||||
| "ja"
|
||||
| "pl"
|
||||
| "ru"
|
||||
| "ar"
|
||||
| "no"
|
||||
| "br"
|
||||
| "th"
|
||||
|
||||
type RawDictionary = typeof en & typeof uiEn
|
||||
type Dictionary = i18n.Flatten<RawDictionary>
|
||||
|
||||
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
|
||||
const LOCALES: readonly Locale[] = [
|
||||
"en",
|
||||
"zh",
|
||||
"zht",
|
||||
"ko",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"da",
|
||||
"ja",
|
||||
"pl",
|
||||
"ru",
|
||||
"ar",
|
||||
"no",
|
||||
"br",
|
||||
"th",
|
||||
]
|
||||
|
||||
function detectLocale(): Locale {
|
||||
if (typeof navigator !== "object") return "en"
|
||||
@@ -65,6 +98,7 @@ function detectLocale(): Locale {
|
||||
)
|
||||
return "no"
|
||||
if (language.toLowerCase().startsWith("pt")) return "br"
|
||||
if (language.toLowerCase().startsWith("th")) return "th"
|
||||
}
|
||||
|
||||
return "en"
|
||||
@@ -94,6 +128,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
if (store.locale === "ar") return "ar"
|
||||
if (store.locale === "no") return "no"
|
||||
if (store.locale === "br") return "br"
|
||||
if (store.locale === "th") return "th"
|
||||
return "en"
|
||||
})
|
||||
|
||||
@@ -118,6 +153,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }
|
||||
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
|
||||
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
|
||||
if (locale() === "th") return { ...base, ...i18n.flatten({ ...th, ...uiTh }) }
|
||||
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
|
||||
})
|
||||
|
||||
@@ -138,6 +174,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
ar: "language.ar",
|
||||
no: "language.no",
|
||||
br: "language.br",
|
||||
th: "language.th",
|
||||
}
|
||||
|
||||
const label = (value: Locale) => t(labelKey[value])
|
||||
|
||||
@@ -51,16 +51,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
|
||||
const migrate = (value: unknown) => {
|
||||
if (!isRecord(value)) return value
|
||||
|
||||
const sidebar = value.sidebar
|
||||
if (!isRecord(sidebar)) return value
|
||||
if (typeof sidebar.workspaces !== "boolean") return value
|
||||
return {
|
||||
...value,
|
||||
sidebar: {
|
||||
const migratedSidebar = (() => {
|
||||
if (!isRecord(sidebar)) return sidebar
|
||||
if (typeof sidebar.workspaces !== "boolean") return sidebar
|
||||
return {
|
||||
...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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +101,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
},
|
||||
review: {
|
||||
diffStyle: "split" as ReviewDiffStyle,
|
||||
panelOpened: true,
|
||||
},
|
||||
fileTree: {
|
||||
opened: true,
|
||||
width: 344,
|
||||
tab: "changes" as "changes" | "all",
|
||||
},
|
||||
session: {
|
||||
width: 600,
|
||||
@@ -218,7 +243,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
|
||||
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||
const [childStore] = globalSync.child(project.worktree)
|
||||
const [childStore] = globalSync.child(project.worktree, { bootstrap: false })
|
||||
const projectID = childStore.project
|
||||
const metadata = projectID
|
||||
? globalSync.data.project.find((x) => x.id === projectID)
|
||||
@@ -449,6 +474,46 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
setStore("review", "diffStyle", diffStyle)
|
||||
},
|
||||
},
|
||||
fileTree: {
|
||||
opened: createMemo(() => store.fileTree?.opened ?? true),
|
||||
width: createMemo(() => store.fileTree?.width ?? 344),
|
||||
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() {
|
||||
if (!store.fileTree) {
|
||||
setStore("fileTree", { opened: true, width: 344, tab: "changes" })
|
||||
return
|
||||
}
|
||||
setStore("fileTree", "opened", true)
|
||||
},
|
||||
close() {
|
||||
if (!store.fileTree) {
|
||||
setStore("fileTree", { opened: false, width: 344, tab: "changes" })
|
||||
return
|
||||
}
|
||||
setStore("fileTree", "opened", false)
|
||||
},
|
||||
toggle() {
|
||||
if (!store.fileTree) {
|
||||
setStore("fileTree", { opened: true, width: 344, tab: "changes" })
|
||||
return
|
||||
}
|
||||
setStore("fileTree", "opened", (x) => !x)
|
||||
},
|
||||
resize(width: number) {
|
||||
if (!store.fileTree) {
|
||||
setStore("fileTree", { opened: true, width, tab: "changes" })
|
||||
return
|
||||
}
|
||||
setStore("fileTree", "width", width)
|
||||
},
|
||||
},
|
||||
session: {
|
||||
width: createMemo(() => store.session?.width ?? 600),
|
||||
resize(width: number) {
|
||||
@@ -490,7 +555,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
|
||||
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
||||
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
||||
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
|
||||
|
||||
function setTerminalOpened(next: boolean) {
|
||||
const current = store.terminal
|
||||
@@ -504,18 +568,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
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 {
|
||||
scroll(tab: string) {
|
||||
return scroll.scroll(key(), tab)
|
||||
@@ -535,18 +587,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
setTerminalOpened(!terminalOpened())
|
||||
},
|
||||
},
|
||||
reviewPanel: {
|
||||
opened: reviewPanelOpened,
|
||||
open() {
|
||||
setReviewPanelOpened(true)
|
||||
},
|
||||
close() {
|
||||
setReviewPanelOpened(false)
|
||||
},
|
||||
toggle() {
|
||||
setReviewPanelOpened(!reviewPanelOpened())
|
||||
},
|
||||
},
|
||||
review: {
|
||||
open: createMemo(() => s().reviewOpen),
|
||||
setOpen(open: string[]) {
|
||||
@@ -584,10 +624,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
||||
return {
|
||||
tabs,
|
||||
active: createMemo(() => tabs().active),
|
||||
all: createMemo(() => tabs().all),
|
||||
active: createMemo(() => (tabs().active === "review" ? undefined : tabs().active)),
|
||||
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
|
||||
setActive(tab: string | undefined) {
|
||||
const session = key()
|
||||
if (tab === "review") return
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all: [], active: tab })
|
||||
} else {
|
||||
@@ -596,25 +637,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
},
|
||||
setAll(all: string[]) {
|
||||
const session = key()
|
||||
const next = all.filter((tab) => tab !== "review")
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all, active: undefined })
|
||||
setStore("sessionTabs", session, { all: next, active: undefined })
|
||||
} else {
|
||||
setStore("sessionTabs", session, "all", all)
|
||||
setStore("sessionTabs", session, "all", next)
|
||||
}
|
||||
},
|
||||
async open(tab: string) {
|
||||
if (tab === "review") return
|
||||
const session = key()
|
||||
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") {
|
||||
const all = [tab, ...current.all.filter((x) => x !== tab)]
|
||||
if (!store.sessionTabs[session]) {
|
||||
|
||||
@@ -1,49 +1,20 @@
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { batch, createMemo } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { DateTime } from "luxon"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useModels } from "@/context/models"
|
||||
|
||||
export type LocalFile = FileNode &
|
||||
Partial<{
|
||||
loaded: boolean
|
||||
pinned: boolean
|
||||
expanded: boolean
|
||||
content: FileContent
|
||||
selection: { startLine: number; startChar: number; endLine: number; endChar: number }
|
||||
scrollTop: number
|
||||
view: "raw" | "diff-unified" | "diff-split"
|
||||
folded: string[]
|
||||
selectedChange: number
|
||||
status: FileStatus
|
||||
}>
|
||||
export type TextSelection = LocalFile["selection"]
|
||||
export type View = LocalFile["view"]
|
||||
|
||||
export type LocalModel = Omit<Model, "provider"> & {
|
||||
provider: Provider
|
||||
latest?: boolean
|
||||
}
|
||||
export type ModelKey = { providerID: string; modelID: string }
|
||||
|
||||
export type FileContext = { type: "file"; path: string; selection?: TextSelection }
|
||||
export type ContextItem = FileContext
|
||||
|
||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
name: "Local",
|
||||
init: () => {
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const providers = useProviders()
|
||||
const language = useLanguage()
|
||||
|
||||
function isModelValid(model: ModelKey) {
|
||||
const provider = providers.all().find((x) => x.id === model.providerID)
|
||||
@@ -112,18 +83,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})()
|
||||
|
||||
const model = (() => {
|
||||
const [store, setStore, _, modelReady] = persisted(
|
||||
Persist.global("model", ["model.v1"]),
|
||||
createStore<{
|
||||
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
|
||||
recent: ModelKey[]
|
||||
variant?: Record<string, string | undefined>
|
||||
}>({
|
||||
user: [],
|
||||
recent: [],
|
||||
variant: {},
|
||||
}),
|
||||
)
|
||||
const models = useModels()
|
||||
|
||||
const [ephemeral, setEphemeral] = createStore<{
|
||||
model: Record<string, ModelKey | undefined>
|
||||
@@ -131,57 +91,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
model: {},
|
||||
})
|
||||
|
||||
const available = createMemo(() =>
|
||||
providers.connected().flatMap((p) =>
|
||||
Object.values(p.models).map((m) => ({
|
||||
...m,
|
||||
provider: p,
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
const latest = createMemo(() =>
|
||||
pipe(
|
||||
available(),
|
||||
filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
|
||||
groupBy((x) => x.provider.id),
|
||||
mapValues((models) =>
|
||||
pipe(
|
||||
models,
|
||||
groupBy((x) => x.family),
|
||||
values(),
|
||||
(groups) =>
|
||||
groups.flatMap((g) => {
|
||||
const first = firstBy(g, [(x) => x.release_date, "desc"])
|
||||
return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
|
||||
}),
|
||||
),
|
||||
),
|
||||
values(),
|
||||
flat(),
|
||||
),
|
||||
)
|
||||
|
||||
const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
|
||||
|
||||
const userVisibilityMap = createMemo(() => {
|
||||
const map = new Map<string, "show" | "hide">()
|
||||
for (const item of store.user) {
|
||||
map.set(`${item.providerID}:${item.modelID}`, item.visibility)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const list = createMemo(() =>
|
||||
available().map((m) => ({
|
||||
...m,
|
||||
name: m.name.replace("(latest)", "").trim(),
|
||||
latest: m.name.includes("(latest)"),
|
||||
})),
|
||||
)
|
||||
|
||||
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
|
||||
|
||||
const fallbackModel = createMemo<ModelKey | undefined>(() => {
|
||||
if (sync.data.config.model) {
|
||||
const [providerID, modelID] = sync.data.config.model.split("/")
|
||||
@@ -193,7 +102,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of store.recent) {
|
||||
for (const item of models.recent.list()) {
|
||||
if (isModelValid(item)) {
|
||||
return item
|
||||
}
|
||||
@@ -225,10 +134,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
fallbackModel,
|
||||
)
|
||||
if (!key) return undefined
|
||||
return find(key)
|
||||
return models.find(key)
|
||||
})
|
||||
|
||||
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
|
||||
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
||||
|
||||
const cycle = (direction: 1 | -1) => {
|
||||
const recentList = recent()
|
||||
@@ -253,54 +162,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
}
|
||||
|
||||
function updateVisibility(model: ModelKey, visibility: "show" | "hide") {
|
||||
const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
|
||||
if (index >= 0) {
|
||||
setStore("user", index, { visibility })
|
||||
} else {
|
||||
setStore("user", store.user.length, { ...model, visibility })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ready: modelReady,
|
||||
ready: models.ready,
|
||||
current,
|
||||
recent,
|
||||
list,
|
||||
list: models.list,
|
||||
cycle,
|
||||
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
const currentAgent = agent.current()
|
||||
const next = model ?? fallbackModel()
|
||||
if (currentAgent) setEphemeral("model", currentAgent.name, next)
|
||||
if (model) updateVisibility(model, "show")
|
||||
if (options?.recent && model) {
|
||||
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 5) uniq.pop()
|
||||
setStore("recent", uniq)
|
||||
}
|
||||
if (model) models.setVisibility(model, true)
|
||||
if (options?.recent && model) models.recent.push(model)
|
||||
})
|
||||
},
|
||||
visible(model: ModelKey) {
|
||||
const key = `${model.providerID}:${model.modelID}`
|
||||
const visibility = userVisibilityMap().get(key)
|
||||
if (visibility === "hide") return false
|
||||
if (visibility === "show") return true
|
||||
if (latestSet().has(key)) return true
|
||||
// For models without valid release_date (e.g. custom models), show by default
|
||||
const m = find(model)
|
||||
if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
|
||||
return false
|
||||
return models.visible(model)
|
||||
},
|
||||
setVisibility(model: ModelKey, visible: boolean) {
|
||||
updateVisibility(model, visible ? "show" : "hide")
|
||||
models.setVisibility(model, visible)
|
||||
},
|
||||
variant: {
|
||||
current() {
|
||||
const m = current()
|
||||
if (!m) return undefined
|
||||
const key = `${m.provider.id}/${m.id}`
|
||||
return store.variant?.[key]
|
||||
return models.variant.get({ providerID: m.provider.id, modelID: m.id })
|
||||
},
|
||||
list() {
|
||||
const m = current()
|
||||
@@ -311,12 +198,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
set(value: string | undefined) {
|
||||
const m = current()
|
||||
if (!m) return
|
||||
const key = `${m.provider.id}/${m.id}`
|
||||
if (!store.variant) {
|
||||
setStore("variant", { [key]: value })
|
||||
} else {
|
||||
setStore("variant", key, value)
|
||||
}
|
||||
models.variant.set({ providerID: m.provider.id, modelID: m.id }, value)
|
||||
},
|
||||
cycle() {
|
||||
const variants = this.list()
|
||||
@@ -337,247 +219,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
})()
|
||||
|
||||
const file = (() => {
|
||||
const [store, setStore] = createStore<{
|
||||
node: Record<string, LocalFile>
|
||||
}>({
|
||||
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
|
||||
})
|
||||
|
||||
const scope = createMemo(() => sdk.directory)
|
||||
createEffect(() => {
|
||||
scope()
|
||||
setStore("node", {})
|
||||
})
|
||||
|
||||
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
|
||||
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
|
||||
|
||||
// createEffect((prev: FileStatus[]) => {
|
||||
// const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path))
|
||||
// for (const p of removed) {
|
||||
// setStore(
|
||||
// "node",
|
||||
// p.path,
|
||||
// produce((draft) => {
|
||||
// draft.status = undefined
|
||||
// draft.view = "raw"
|
||||
// }),
|
||||
// )
|
||||
// load(p.path)
|
||||
// }
|
||||
// for (const p of sync.data.changes) {
|
||||
// if (store.node[p.path] === undefined) {
|
||||
// fetch(p.path).then(() => {
|
||||
// if (store.node[p.path] === undefined) return
|
||||
// setStore("node", p.path, "status", p)
|
||||
// })
|
||||
// } else {
|
||||
// setStore("node", p.path, "status", p)
|
||||
// }
|
||||
// }
|
||||
// return sync.data.changes
|
||||
// }, sync.data.changes)
|
||||
|
||||
// const changed = (path: string) => {
|
||||
// const node = store.node[path]
|
||||
// if (node?.status) return true
|
||||
// const set = changeset()
|
||||
// if (set.has(path)) return true
|
||||
// for (const p of set) {
|
||||
// if (p.startsWith(path ? path + "/" : "")) return true
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
|
||||
// const resetNode = (path: string) => {
|
||||
// setStore("node", path, {
|
||||
// loaded: undefined,
|
||||
// pinned: undefined,
|
||||
// content: undefined,
|
||||
// selection: undefined,
|
||||
// scrollTop: undefined,
|
||||
// folded: undefined,
|
||||
// view: undefined,
|
||||
// selectedChange: undefined,
|
||||
// })
|
||||
// }
|
||||
|
||||
const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
|
||||
|
||||
const load = async (path: string) => {
|
||||
const directory = scope()
|
||||
const client = sdk.client
|
||||
const relativePath = relative(path)
|
||||
await client.file
|
||||
.read({ path: relativePath })
|
||||
.then((x) => {
|
||||
if (scope() !== directory) return
|
||||
if (!store.node[relativePath]) return
|
||||
setStore(
|
||||
"node",
|
||||
relativePath,
|
||||
produce((draft) => {
|
||||
draft.loaded = true
|
||||
draft.content = x.data
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
if (scope() !== directory) return
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.file.loadFailed.title"),
|
||||
description: e.message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const fetch = async (path: string) => {
|
||||
const relativePath = relative(path)
|
||||
const parent = relativePath.split("/").slice(0, -1).join("/")
|
||||
if (parent) {
|
||||
await list(parent)
|
||||
}
|
||||
}
|
||||
|
||||
const init = async (path: string) => {
|
||||
const relativePath = relative(path)
|
||||
if (!store.node[relativePath]) await fetch(path)
|
||||
if (store.node[relativePath]?.loaded) return
|
||||
return load(relativePath)
|
||||
}
|
||||
|
||||
const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => {
|
||||
const relativePath = relative(path)
|
||||
if (!store.node[relativePath]) await fetch(path)
|
||||
// setStore("opened", (x) => {
|
||||
// if (x.includes(relativePath)) return x
|
||||
// return [
|
||||
// ...opened()
|
||||
// .filter((x) => x.pinned)
|
||||
// .map((x) => x.path),
|
||||
// relativePath,
|
||||
// ]
|
||||
// })
|
||||
// setStore("active", relativePath)
|
||||
// context.addActive()
|
||||
if (options?.pinned) setStore("node", path, "pinned", true)
|
||||
if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
|
||||
if (store.node[relativePath]?.loaded) return
|
||||
return load(relativePath)
|
||||
}
|
||||
|
||||
const list = async (path: string) => {
|
||||
const directory = scope()
|
||||
const client = sdk.client
|
||||
return client.file
|
||||
.list({ path: path + "/" })
|
||||
.then((x) => {
|
||||
if (scope() !== directory) return
|
||||
setStore(
|
||||
"node",
|
||||
produce((draft) => {
|
||||
x.data!.forEach((node) => {
|
||||
if (node.path in draft) return
|
||||
draft[node.path] = node
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
|
||||
const searchFilesAndDirectories = (query: string) =>
|
||||
sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
|
||||
|
||||
const unsub = sdk.event.listen((e) => {
|
||||
const event = e.details
|
||||
switch (event.type) {
|
||||
case "file.watcher.updated":
|
||||
const relativePath = relative(event.properties.file)
|
||||
if (relativePath.startsWith(".git/")) return
|
||||
if (store.node[relativePath]) load(relativePath)
|
||||
break
|
||||
}
|
||||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
return {
|
||||
node: async (path: string) => {
|
||||
if (!store.node[path] || !store.node[path].loaded) {
|
||||
await init(path)
|
||||
}
|
||||
return store.node[path]
|
||||
},
|
||||
update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)),
|
||||
open,
|
||||
load,
|
||||
init,
|
||||
expand(path: string) {
|
||||
setStore("node", path, "expanded", true)
|
||||
if (store.node[path]?.loaded) return
|
||||
setStore("node", path, "loaded", true)
|
||||
list(path)
|
||||
},
|
||||
collapse(path: string) {
|
||||
setStore("node", path, "expanded", false)
|
||||
},
|
||||
select(path: string, selection: TextSelection | undefined) {
|
||||
setStore("node", path, "selection", selection)
|
||||
},
|
||||
scroll(path: string, scrollTop: number) {
|
||||
setStore("node", path, "scrollTop", scrollTop)
|
||||
},
|
||||
view(path: string): View {
|
||||
const n = store.node[path]
|
||||
return n && n.view ? n.view : "raw"
|
||||
},
|
||||
setView(path: string, view: View) {
|
||||
setStore("node", path, "view", view)
|
||||
},
|
||||
unfold(path: string, key: string) {
|
||||
setStore("node", path, "folded", (xs) => {
|
||||
const a = xs ?? []
|
||||
if (a.includes(key)) return a
|
||||
return [...a, key]
|
||||
})
|
||||
},
|
||||
fold(path: string, key: string) {
|
||||
setStore("node", path, "folded", (xs) => (xs ?? []).filter((k) => k !== key))
|
||||
},
|
||||
folded(path: string) {
|
||||
const n = store.node[path]
|
||||
return n && n.folded ? n.folded : []
|
||||
},
|
||||
changeIndex(path: string) {
|
||||
return store.node[path]?.selectedChange
|
||||
},
|
||||
setChangeIndex(path: string, index: number | undefined) {
|
||||
setStore("node", path, "selectedChange", index)
|
||||
},
|
||||
// changes,
|
||||
// changed,
|
||||
children(path: string) {
|
||||
return Object.values(store.node).filter(
|
||||
(x) =>
|
||||
x.path.startsWith(path) &&
|
||||
x.path !== path &&
|
||||
!x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"),
|
||||
)
|
||||
},
|
||||
searchFiles,
|
||||
searchFilesAndDirectories,
|
||||
relative,
|
||||
}
|
||||
})()
|
||||
|
||||
const result = {
|
||||
slug: createMemo(() => base64Encode(sdk.directory)),
|
||||
model,
|
||||
agent,
|
||||
file,
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
140
packages/app/src/context/models.tsx
Normal file
140
packages/app/src/context/models.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { DateTime } from "luxon"
|
||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
export type ModelKey = { providerID: string; modelID: string }
|
||||
|
||||
type Visibility = "show" | "hide"
|
||||
type User = ModelKey & { visibility: Visibility; favorite?: boolean }
|
||||
type Store = {
|
||||
user: User[]
|
||||
recent: ModelKey[]
|
||||
variant?: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
export const { use: useModels, provider: ModelsProvider } = createSimpleContext({
|
||||
name: "Models",
|
||||
init: () => {
|
||||
const providers = useProviders()
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("model", ["model.v1"]),
|
||||
createStore<Store>({
|
||||
user: [],
|
||||
recent: [],
|
||||
variant: {},
|
||||
}),
|
||||
)
|
||||
|
||||
const available = createMemo(() =>
|
||||
providers.connected().flatMap((p) =>
|
||||
Object.values(p.models).map((m) => ({
|
||||
...m,
|
||||
provider: p,
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
const latest = createMemo(() =>
|
||||
pipe(
|
||||
available(),
|
||||
filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
|
||||
groupBy((x) => x.provider.id),
|
||||
mapValues((models) =>
|
||||
pipe(
|
||||
models,
|
||||
groupBy((x) => x.family),
|
||||
values(),
|
||||
(groups) =>
|
||||
groups.flatMap((g) => {
|
||||
const first = firstBy(g, [(x) => x.release_date, "desc"])
|
||||
return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
|
||||
}),
|
||||
),
|
||||
),
|
||||
values(),
|
||||
flat(),
|
||||
),
|
||||
)
|
||||
|
||||
const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
|
||||
|
||||
const visibility = createMemo(() => {
|
||||
const map = new Map<string, Visibility>()
|
||||
for (const item of store.user) map.set(`${item.providerID}:${item.modelID}`, item.visibility)
|
||||
return map
|
||||
})
|
||||
|
||||
const list = createMemo(() =>
|
||||
available().map((m) => ({
|
||||
...m,
|
||||
name: m.name.replace("(latest)", "").trim(),
|
||||
latest: m.name.includes("(latest)"),
|
||||
})),
|
||||
)
|
||||
|
||||
const find = (key: ModelKey) => list().find((m) => m.id === key.modelID && m.provider.id === key.providerID)
|
||||
|
||||
function update(model: ModelKey, state: Visibility) {
|
||||
const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
|
||||
if (index >= 0) {
|
||||
setStore("user", index, { visibility: state })
|
||||
return
|
||||
}
|
||||
setStore("user", store.user.length, { ...model, visibility: state })
|
||||
}
|
||||
|
||||
const visible = (model: ModelKey) => {
|
||||
const key = `${model.providerID}:${model.modelID}`
|
||||
const state = visibility().get(key)
|
||||
if (state === "hide") return false
|
||||
if (state === "show") return true
|
||||
if (latestSet().has(key)) return true
|
||||
const m = find(model)
|
||||
if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const setVisibility = (model: ModelKey, state: boolean) => {
|
||||
update(model, state ? "show" : "hide")
|
||||
}
|
||||
|
||||
const push = (model: ModelKey) => {
|
||||
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 5) uniq.pop()
|
||||
setStore("recent", uniq)
|
||||
}
|
||||
|
||||
const variantKey = (model: ModelKey) => `${model.providerID}/${model.modelID}`
|
||||
const getVariant = (model: ModelKey) => store.variant?.[variantKey(model)]
|
||||
|
||||
const setVariant = (model: ModelKey, value: string | undefined) => {
|
||||
const key = variantKey(model)
|
||||
if (!store.variant) {
|
||||
setStore("variant", { [key]: value })
|
||||
return
|
||||
}
|
||||
setStore("variant", key, value)
|
||||
}
|
||||
|
||||
return {
|
||||
ready,
|
||||
list,
|
||||
find,
|
||||
visible,
|
||||
setVisibility,
|
||||
recent: {
|
||||
list: createMemo(() => store.recent),
|
||||
push,
|
||||
},
|
||||
variant: {
|
||||
get: getVariant,
|
||||
set: setVariant,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createEffect, onCleanup } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
@@ -8,7 +8,8 @@ import { usePlatform } from "@/context/platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { playSound, soundSrc } from "@/utils/sound"
|
||||
@@ -52,6 +53,14 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
const settings = useSettings()
|
||||
const language = useLanguage()
|
||||
|
||||
const empty: Notification[] = []
|
||||
|
||||
const currentDirectory = createMemo(() => {
|
||||
return decode64(params.dir)
|
||||
})
|
||||
|
||||
const currentSession = createMemo(() => params.id)
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("notification", ["notification.v1"]),
|
||||
createStore({
|
||||
@@ -72,13 +81,59 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
setStore("list", (list) => pruneNotifications([...list, notification]))
|
||||
}
|
||||
|
||||
const index = createMemo(() => {
|
||||
const sessionAll = new Map<string, Notification[]>()
|
||||
const sessionUnseen = new Map<string, Notification[]>()
|
||||
const projectAll = new Map<string, Notification[]>()
|
||||
const projectUnseen = new Map<string, Notification[]>()
|
||||
|
||||
for (const notification of store.list) {
|
||||
const session = notification.session
|
||||
if (session) {
|
||||
const list = sessionAll.get(session)
|
||||
if (list) list.push(notification)
|
||||
else sessionAll.set(session, [notification])
|
||||
if (!notification.viewed) {
|
||||
const unseen = sessionUnseen.get(session)
|
||||
if (unseen) unseen.push(notification)
|
||||
else sessionUnseen.set(session, [notification])
|
||||
}
|
||||
}
|
||||
|
||||
const directory = notification.directory
|
||||
if (directory) {
|
||||
const list = projectAll.get(directory)
|
||||
if (list) list.push(notification)
|
||||
else projectAll.set(directory, [notification])
|
||||
if (!notification.viewed) {
|
||||
const unseen = projectUnseen.get(directory)
|
||||
if (unseen) unseen.push(notification)
|
||||
else projectUnseen.set(directory, [notification])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
session: {
|
||||
all: sessionAll,
|
||||
unseen: sessionUnseen,
|
||||
},
|
||||
project: {
|
||||
all: projectAll,
|
||||
unseen: projectUnseen,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
if (event.type !== "session.idle" && event.type !== "session.error") return
|
||||
|
||||
const directory = e.name
|
||||
const time = Date.now()
|
||||
const activeDirectory = params.dir ? base64Decode(params.dir) : undefined
|
||||
const activeSession = params.id
|
||||
const viewed = (sessionID?: string) => {
|
||||
const activeDirectory = currentDirectory()
|
||||
const activeSession = currentSession()
|
||||
if (!activeDirectory) return false
|
||||
if (!activeSession) return false
|
||||
if (!sessionID) return false
|
||||
@@ -88,7 +143,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
switch (event.type) {
|
||||
case "session.idle": {
|
||||
const sessionID = event.properties.sessionID
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const [syncStore] = globalSync.child(directory, { bootstrap: false })
|
||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||
const session = match.found ? syncStore.session[match.index] : undefined
|
||||
if (session?.parentID) break
|
||||
@@ -115,7 +170,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
}
|
||||
case "session.error": {
|
||||
const sessionID = event.properties.sessionID
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const [syncStore] = globalSync.child(directory, { bootstrap: false })
|
||||
const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
|
||||
const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
|
||||
if (session?.parentID) break
|
||||
@@ -148,10 +203,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
ready,
|
||||
session: {
|
||||
all(session: string) {
|
||||
return store.list.filter((n) => n.session === session)
|
||||
return index().session.all.get(session) ?? empty
|
||||
},
|
||||
unseen(session: string) {
|
||||
return store.list.filter((n) => n.session === session && !n.viewed)
|
||||
return index().session.unseen.get(session) ?? empty
|
||||
},
|
||||
markViewed(session: string) {
|
||||
setStore("list", (n) => n.session === session, "viewed", true)
|
||||
@@ -159,10 +214,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
},
|
||||
project: {
|
||||
all(directory: string) {
|
||||
return store.list.filter((n) => n.directory === directory)
|
||||
return index().project.all.get(directory) ?? empty
|
||||
},
|
||||
unseen(directory: string) {
|
||||
return store.list.filter((n) => n.directory === directory && !n.viewed)
|
||||
return index().project.unseen.get(directory) ?? empty
|
||||
},
|
||||
markViewed(directory: string) {
|
||||
setStore("list", (n) => n.directory === directory, "viewed", true)
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Persist, persisted } from "@/utils/persist"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
|
||||
type PermissionRespondFn = (input: {
|
||||
sessionID: string
|
||||
@@ -53,7 +54,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
const globalSync = useGlobalSync()
|
||||
|
||||
const permissionsEnabled = createMemo(() => {
|
||||
const directory = params.dir ? base64Decode(params.dir) : undefined
|
||||
const directory = decode64(params.dir)
|
||||
if (!directory) return false
|
||||
const [store] = globalSync.child(directory)
|
||||
return hasAutoAcceptPermissionConfig(store.config.permission)
|
||||
@@ -66,7 +67,21 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
}),
|
||||
)
|
||||
|
||||
const responded = new Set<string>()
|
||||
const MAX_RESPONDED = 1000
|
||||
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) => {
|
||||
globalSDK.client.permission.respond(input).catch(() => {
|
||||
@@ -75,8 +90,12 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
}
|
||||
|
||||
function respondOnce(permission: PermissionRequest, directory?: string) {
|
||||
if (responded.has(permission.id)) return
|
||||
responded.add(permission.id)
|
||||
const now = Date.now()
|
||||
const hit = responded.has(permission.id)
|
||||
responded.delete(permission.id)
|
||||
responded.set(permission.id, now)
|
||||
pruneResponded(now)
|
||||
if (hit) return
|
||||
respond({
|
||||
sessionID: permission.sessionID,
|
||||
permissionID: permission.id,
|
||||
|
||||
@@ -17,6 +17,12 @@ export type Platform = {
|
||||
/** Restart the app */
|
||||
restart(): Promise<void>
|
||||
|
||||
/** Navigate back in history */
|
||||
back(): void
|
||||
|
||||
/** Navigate forward in history */
|
||||
forward(): void
|
||||
|
||||
/** Send a system notification (optional deep link) */
|
||||
notify(title: string, description?: string, href?: string): Promise<void>
|
||||
|
||||
@@ -41,11 +47,11 @@ export type Platform = {
|
||||
/** Fetch override */
|
||||
fetch?: typeof fetch
|
||||
|
||||
/** Get the configured default server URL (desktop only) */
|
||||
getDefaultServerUrl?(): Promise<string | null>
|
||||
/** Get the configured default server URL (platform-specific) */
|
||||
getDefaultServerUrl?(): Promise<string | null> | string | null
|
||||
|
||||
/** Set the default server URL to use on app startup (desktop only) */
|
||||
setDefaultServerUrl?(url: string | null): Promise<void>
|
||||
/** Set the default server URL to use on app startup (platform-specific) */
|
||||
setDefaultServerUrl?(url: string | null): Promise<void> | void
|
||||
|
||||
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
|
||||
parseMarkdown?(markdown: string): Promise<string>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
@@ -40,12 +40,17 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
}),
|
||||
)
|
||||
|
||||
const [active, setActiveRaw] = createSignal("")
|
||||
const [state, setState] = createStore({
|
||||
active: "",
|
||||
healthy: undefined as boolean | undefined,
|
||||
})
|
||||
|
||||
const healthy = () => state.healthy
|
||||
|
||||
function setActive(input: string) {
|
||||
const url = normalizeServerUrl(input)
|
||||
if (!url) return
|
||||
setActiveRaw(url)
|
||||
setState("active", url)
|
||||
}
|
||||
|
||||
function add(input: string) {
|
||||
@@ -54,7 +59,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
|
||||
const fallback = normalizeServerUrl(props.defaultUrl)
|
||||
if (fallback && url === fallback) {
|
||||
setActiveRaw(url)
|
||||
setState("active", url)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,7 +67,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
if (!store.list.includes(url)) {
|
||||
setStore("list", store.list.length, url)
|
||||
}
|
||||
setActiveRaw(url)
|
||||
setState("active", url)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,31 +76,30 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
if (!url) return
|
||||
|
||||
const list = store.list.filter((x) => x !== url)
|
||||
const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active()
|
||||
const next = state.active === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : state.active
|
||||
|
||||
batch(() => {
|
||||
setStore("list", list)
|
||||
setActiveRaw(next)
|
||||
setState("active", next)
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
if (active()) return
|
||||
if (state.active) return
|
||||
const url = normalizeServerUrl(props.defaultUrl)
|
||||
if (!url) return
|
||||
setActiveRaw(url)
|
||||
setState("active", url)
|
||||
})
|
||||
|
||||
const isReady = createMemo(() => ready() && !!active())
|
||||
|
||||
const [healthy, setHealthy] = createSignal<boolean | undefined>(undefined)
|
||||
const isReady = createMemo(() => ready() && !!state.active)
|
||||
|
||||
const check = (url: string) => {
|
||||
const signal = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(3000)
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: url,
|
||||
fetch: platform.fetch,
|
||||
signal: AbortSignal.timeout(3000),
|
||||
signal,
|
||||
})
|
||||
return sdk.global
|
||||
.health()
|
||||
@@ -104,10 +108,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const url = active()
|
||||
const url = state.active
|
||||
if (!url) return
|
||||
|
||||
setHealthy(undefined)
|
||||
setState("healthy", undefined)
|
||||
|
||||
let alive = true
|
||||
let busy = false
|
||||
@@ -118,7 +122,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
void check(url)
|
||||
.then((next) => {
|
||||
if (!alive) return
|
||||
setHealthy(next)
|
||||
setState("healthy", next)
|
||||
})
|
||||
.finally(() => {
|
||||
busy = false
|
||||
@@ -134,7 +138,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
})
|
||||
})
|
||||
|
||||
const origin = createMemo(() => projectsKey(active()))
|
||||
const origin = createMemo(() => projectsKey(state.active))
|
||||
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
||||
const isLocal = createMemo(() => origin() === "local")
|
||||
|
||||
@@ -143,10 +147,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
healthy,
|
||||
isLocal,
|
||||
get url() {
|
||||
return active()
|
||||
return state.active
|
||||
},
|
||||
get name() {
|
||||
return serverDisplayName(active())
|
||||
return serverDisplayName(state.active)
|
||||
},
|
||||
get list() {
|
||||
return store.list
|
||||
|
||||
@@ -18,6 +18,10 @@ export interface SoundSettings {
|
||||
export interface Settings {
|
||||
general: {
|
||||
autoSave: boolean
|
||||
releaseNotes: boolean
|
||||
}
|
||||
updates: {
|
||||
startup: boolean
|
||||
}
|
||||
appearance: {
|
||||
fontSize: number
|
||||
@@ -34,6 +38,10 @@ export interface Settings {
|
||||
const defaultSettings: Settings = {
|
||||
general: {
|
||||
autoSave: true,
|
||||
releaseNotes: true,
|
||||
},
|
||||
updates: {
|
||||
startup: true,
|
||||
},
|
||||
appearance: {
|
||||
fontSize: 14,
|
||||
@@ -97,6 +105,16 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setAutoSave(value: boolean) {
|
||||
setStore("general", "autoSave", value)
|
||||
},
|
||||
releaseNotes: createMemo(() => store.general?.releaseNotes ?? defaultSettings.general.releaseNotes),
|
||||
setReleaseNotes(value: boolean) {
|
||||
setStore("general", "releaseNotes", value)
|
||||
},
|
||||
},
|
||||
updates: {
|
||||
startup: createMemo(() => store.updates?.startup ?? defaultSettings.updates.startup),
|
||||
setStartup(value: boolean) {
|
||||
setStore("updates", "startup", value)
|
||||
},
|
||||
},
|
||||
appearance: {
|
||||
fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
|
||||
|
||||
@@ -16,7 +16,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const sdk = useSDK()
|
||||
|
||||
type Child = ReturnType<(typeof globalSync)["child"]>
|
||||
type Store = Child[0]
|
||||
type Setter = Child[1]
|
||||
|
||||
const current = createMemo(() => globalSync.child(sdk.directory))
|
||||
@@ -43,18 +42,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return Math.ceil(count / chunk) * chunk
|
||||
}
|
||||
|
||||
const hydrateMessages = (directory: string, store: Store, sessionID: string) => {
|
||||
const key = keyFor(directory, sessionID)
|
||||
if (meta.limit[key] !== undefined) return
|
||||
|
||||
const messages = store.message[sessionID]
|
||||
if (!messages) return
|
||||
|
||||
const limit = limitFor(messages.length)
|
||||
setMeta("limit", key, limit)
|
||||
setMeta("complete", key, messages.length < limit)
|
||||
}
|
||||
|
||||
const loadMessages = async (input: {
|
||||
directory: string
|
||||
client: typeof sdk.client
|
||||
@@ -150,21 +137,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const hasSession = (() => {
|
||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
return match.found
|
||||
})()
|
||||
|
||||
hydrateMessages(directory, store, sessionID)
|
||||
|
||||
const hasMessages = store.message[sessionID] !== undefined
|
||||
if (hasSession && hasMessages) return
|
||||
|
||||
const key = keyFor(directory, sessionID)
|
||||
const hydrated = meta.limit[key] !== undefined
|
||||
if (hasSession && hasMessages && hydrated) return
|
||||
const pending = inflight.get(key)
|
||||
if (pending) return pending
|
||||
|
||||
const limit = meta.limit[key] ?? chunk
|
||||
const count = store.message[sessionID]?.length ?? 0
|
||||
const limit = hydrated ? (meta.limit[key] ?? chunk) : limitFor(count)
|
||||
|
||||
const sessionReq = hasSession
|
||||
? Promise.resolve()
|
||||
@@ -184,15 +170,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
)
|
||||
})
|
||||
|
||||
const messagesReq = hasMessages
|
||||
? Promise.resolve()
|
||||
: loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
const messagesReq =
|
||||
hasMessages && hydrated
|
||||
? Promise.resolve()
|
||||
: loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
|
||||
const promise = Promise.all([sessionReq, messagesReq])
|
||||
.then(() => {})
|
||||
|
||||
@@ -155,8 +155,9 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, sess
|
||||
|
||||
batch(() => {
|
||||
setStore("all", index, {
|
||||
...pty,
|
||||
...clone.data,
|
||||
id: clone.data.id,
|
||||
title: clone.data.title ?? pty.title,
|
||||
titleNumber: pty.titleNumber,
|
||||
})
|
||||
if (active) {
|
||||
setStore("active", clone.data.id)
|
||||
|
||||
@@ -6,6 +6,8 @@ import { dict as en } from "@/i18n/en"
|
||||
import { dict as zh } from "@/i18n/zh"
|
||||
import pkg from "../package.json"
|
||||
|
||||
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
const locale = (() => {
|
||||
@@ -29,6 +31,12 @@ const platform: Platform = {
|
||||
openLink(url: string) {
|
||||
window.open(url, "_blank")
|
||||
},
|
||||
back() {
|
||||
window.history.back()
|
||||
},
|
||||
forward() {
|
||||
window.history.forward()
|
||||
},
|
||||
restart: async () => {
|
||||
window.location.reload()
|
||||
},
|
||||
@@ -62,6 +70,26 @@ const platform: Platform = {
|
||||
})
|
||||
.catch(() => undefined)
|
||||
},
|
||||
getDefaultServerUrl: () => {
|
||||
if (typeof localStorage === "undefined") return null
|
||||
try {
|
||||
return localStorage.getItem(DEFAULT_SERVER_URL_KEY)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
setDefaultServerUrl: (url) => {
|
||||
if (typeof localStorage === "undefined") return
|
||||
try {
|
||||
if (url) {
|
||||
localStorage.setItem(DEFAULT_SERVER_URL_KEY, url)
|
||||
return
|
||||
}
|
||||
localStorage.removeItem(DEFAULT_SERVER_URL_KEY)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo } from "solid-js"
|
||||
|
||||
@@ -8,7 +8,7 @@ export const popularProviders = ["opencode", "anthropic", "github-copilot", "ope
|
||||
export function useProviders() {
|
||||
const globalSync = useGlobalSync()
|
||||
const params = useParams()
|
||||
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const currentDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||
const providers = createMemo(() => {
|
||||
if (currentDirectory()) {
|
||||
const [projectStore] = globalSync.child(currentDirectory())
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
"command.category.theme": "سمة",
|
||||
"command.category.language": "لغة",
|
||||
"command.category.file": "ملف",
|
||||
"command.category.context": "سياق",
|
||||
"command.category.terminal": "محطة طرفية",
|
||||
"command.category.model": "نموذج",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -42,7 +43,10 @@ export const dict = {
|
||||
"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": "إنشاء علامة تبويب جديدة للمحطة الطرفية",
|
||||
@@ -137,6 +141,8 @@ export const dict = {
|
||||
"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",
|
||||
@@ -159,6 +165,8 @@ export const dict = {
|
||||
"common.loading": "جارٍ التحميل",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "إلغاء",
|
||||
"common.connect": "اتصال",
|
||||
"common.disconnect": "قطع الاتصال",
|
||||
"common.submit": "إرسال",
|
||||
"common.save": "حفظ",
|
||||
"common.saving": "جارٍ الحفظ...",
|
||||
@@ -167,6 +175,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "أدخل أمر shell...",
|
||||
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
||||
"prompt.placeholder.summarizeComment": "لخّص التعليق…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc للخروج",
|
||||
|
||||
@@ -270,6 +280,9 @@ export const dict = {
|
||||
"dialog.project.edit.color": "لون",
|
||||
"dialog.project.edit.color.select": "اختر لون {{color}}",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "سكريبت بدء تشغيل مساحة العمل",
|
||||
"dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "مثال: bun install",
|
||||
"context.breakdown.title": "تفصيل السياق",
|
||||
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال. يشمل "أخرى" تعريفات الأدوات والنفقات العامة.',
|
||||
"context.breakdown.system": "النظام",
|
||||
@@ -318,6 +331,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "لغة",
|
||||
"toast.language.description": "تم التبديل إلى {{language}}",
|
||||
@@ -335,6 +349,9 @@ export const dict = {
|
||||
|
||||
"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 للمشاركة إلى الحافظة!",
|
||||
@@ -408,8 +425,13 @@ export const dict = {
|
||||
"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": "تحميل الرسائل السابقة",
|
||||
@@ -483,12 +505,15 @@ export const dict = {
|
||||
"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": "اللغة",
|
||||
@@ -499,12 +524,25 @@ export const dict = {
|
||||
"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": 'عرض نوافذ "ما الجديد" المنبثقة بعد التحديثات',
|
||||
|
||||
"settings.updates.row.startup.title": "التحقق من التحديثات عند بدء التشغيل",
|
||||
"settings.updates.row.startup.description": "التحقق تلقائيًا من التحديثات عند تشغيل OpenCode",
|
||||
"settings.updates.row.check.title": "التحقق من التحديثات",
|
||||
"settings.updates.row.check.description": "التحقق يدويًا من التحديثات وتثبيتها إذا كانت متاحة",
|
||||
"settings.updates.action.checkNow": "تحقق الآن",
|
||||
"settings.updates.action.checking": "جارٍ التحقق...",
|
||||
"settings.updates.toast.latest.title": "أنت على آخر إصدار",
|
||||
"settings.updates.toast.latest.description": "أنت تستخدم أحدث إصدار من OpenCode.",
|
||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||
"font.option.cascadiaCode": "Cascadia Code",
|
||||
"font.option.firaCode": "Fira Code",
|
||||
"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",
|
||||
@@ -590,6 +628,13 @@ export const dict = {
|
||||
|
||||
"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": "الوكلاء",
|
||||
@@ -657,6 +702,7 @@ export const dict = {
|
||||
"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": "لم يتم اكتشاف تغييرات غير مدمجة.",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
"command.category.theme": "Tema",
|
||||
"command.category.language": "Idioma",
|
||||
"command.category.file": "Arquivo",
|
||||
"command.category.context": "Contexto",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modelo",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -42,7 +43,10 @@ export const dict = {
|
||||
"command.session.new": "Nova sessão",
|
||||
"command.file.open": "Abrir arquivo",
|
||||
"command.file.open.description": "Buscar arquivos e comandos",
|
||||
"command.context.addSelection": "Adicionar seleção ao contexto",
|
||||
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
|
||||
"command.terminal.toggle": "Alternar terminal",
|
||||
"command.fileTree.toggle": "Alternar árvore de arquivos",
|
||||
"command.review.toggle": "Alternar revisão",
|
||||
"command.terminal.new": "Novo terminal",
|
||||
"command.terminal.new.description": "Criar uma nova aba de terminal",
|
||||
@@ -137,6 +141,8 @@ export const dict = {
|
||||
"provider.connect.toast.connected.title": "{{provider}} conectado",
|
||||
"provider.connect.toast.connected.description": "Modelos do {{provider}} agora estão disponíveis para uso.",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} desconectado",
|
||||
"provider.disconnect.toast.disconnected.description": "Os modelos de {{provider}} não estão mais disponíveis.",
|
||||
"model.tag.free": "Grátis",
|
||||
"model.tag.latest": "Mais recente",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
@@ -159,6 +165,8 @@ export const dict = {
|
||||
"common.loading": "Carregando",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.connect": "Conectar",
|
||||
"common.disconnect": "Desconectar",
|
||||
"common.submit": "Enviar",
|
||||
"common.save": "Salvar",
|
||||
"common.saving": "Salvando...",
|
||||
@@ -167,6 +175,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "Digite comando do shell...",
|
||||
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
||||
"prompt.placeholder.summarizeComment": "Resumir comentário…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc para sair",
|
||||
|
||||
@@ -223,6 +233,8 @@ export const dict = {
|
||||
"dialog.mcp.description": "{{enabled}} de {{total}} habilitados",
|
||||
"dialog.mcp.empty": "Nenhum MCP configurado",
|
||||
|
||||
"dialog.lsp.empty": "LSPs detectados automaticamente pelos tipos de arquivo",
|
||||
"dialog.plugins.empty": "Plugins configurados em opencode.json",
|
||||
"mcp.status.connected": "conectado",
|
||||
"mcp.status.failed": "falhou",
|
||||
"mcp.status.needs_auth": "precisa de autenticação",
|
||||
@@ -251,6 +263,12 @@ export const dict = {
|
||||
"dialog.server.default.clear": "Limpar",
|
||||
"dialog.server.action.remove": "Remover servidor",
|
||||
|
||||
"dialog.server.menu.edit": "Editar",
|
||||
"dialog.server.menu.default": "Definir como padrão",
|
||||
"dialog.server.menu.defaultRemove": "Remover padrão",
|
||||
"dialog.server.menu.delete": "Excluir",
|
||||
"dialog.server.current": "Servidor atual",
|
||||
"dialog.server.status.default": "Padrão",
|
||||
"dialog.project.edit.title": "Editar projeto",
|
||||
"dialog.project.edit.name": "Nome",
|
||||
"dialog.project.edit.icon": "Ícone",
|
||||
@@ -312,6 +330,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Idioma",
|
||||
"toast.language.description": "Alterado para {{language}}",
|
||||
@@ -329,6 +348,9 @@ export const dict = {
|
||||
|
||||
"toast.file.loadFailed.title": "Falha ao carregar arquivo",
|
||||
|
||||
"toast.file.listFailed.title": "Falha ao listar arquivos",
|
||||
"toast.context.noLineSelection.title": "Nenhuma seleção de linhas",
|
||||
"toast.context.noLineSelection.description": "Selecione primeiro um intervalo de linhas em uma aba de arquivo.",
|
||||
"toast.session.share.copyFailed.title": "Falha ao copiar URL para a área de transferência",
|
||||
"toast.session.share.success.title": "Sessão compartilhada",
|
||||
"toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!",
|
||||
@@ -404,8 +426,13 @@ export const dict = {
|
||||
"session.tab.context": "Contexto",
|
||||
"session.panel.reviewAndFiles": "Revisão e arquivos",
|
||||
"session.review.filesChanged": "{{count}} Arquivos Alterados",
|
||||
"session.review.change.one": "Alteração",
|
||||
"session.review.change.other": "Alterações",
|
||||
"session.review.loadingChanges": "Carregando alterações...",
|
||||
"session.review.empty": "Nenhuma alteração nesta sessão ainda",
|
||||
"session.review.noChanges": "Sem alterações",
|
||||
"session.files.selectToOpen": "Selecione um arquivo para abrir",
|
||||
"session.files.all": "Todos os arquivos",
|
||||
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
|
||||
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
|
||||
"session.messages.loadEarlier": "Carregar mensagens anteriores",
|
||||
@@ -482,12 +509,15 @@ export const dict = {
|
||||
"sidebar.project.recentSessions": "Sessões recentes",
|
||||
"sidebar.project.viewAllSessions": "Ver todas as sessões",
|
||||
|
||||
"app.name.desktop": "OpenCode Desktop",
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.section.server": "Servidor",
|
||||
"settings.tab.general": "Geral",
|
||||
"settings.tab.shortcuts": "Atalhos",
|
||||
|
||||
"settings.general.section.appearance": "Aparência",
|
||||
"settings.general.section.notifications": "Notificações do sistema",
|
||||
"settings.general.section.updates": "Atualizações",
|
||||
"settings.general.section.sounds": "Efeitos sonoros",
|
||||
|
||||
"settings.general.row.language.title": "Idioma",
|
||||
@@ -498,12 +528,25 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
||||
"settings.general.row.font.title": "Fonte",
|
||||
"settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código",
|
||||
|
||||
"settings.general.row.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.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",
|
||||
@@ -591,6 +634,13 @@ export const dict = {
|
||||
|
||||
"settings.providers.title": "Provedores",
|
||||
"settings.providers.description": "Configurações de provedores estarão disponíveis aqui.",
|
||||
"settings.providers.section.connected": "Provedores conectados",
|
||||
"settings.providers.connected.empty": "Nenhum provedor conectado",
|
||||
"settings.providers.section.popular": "Provedores populares",
|
||||
"settings.providers.tag.environment": "Ambiente",
|
||||
"settings.providers.tag.config": "Configuração",
|
||||
"settings.providers.tag.custom": "Personalizado",
|
||||
"settings.providers.tag.other": "Outro",
|
||||
"settings.models.title": "Modelos",
|
||||
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
|
||||
"settings.agents.title": "Agentes",
|
||||
@@ -658,6 +708,7 @@ export const dict = {
|
||||
"workspace.reset.failed.title": "Falha ao redefinir espaço de trabalho",
|
||||
"workspace.reset.success.title": "Espaço de trabalho redefinido",
|
||||
"workspace.reset.success.description": "Espaço de trabalho agora corresponde ao branch padrão.",
|
||||
"workspace.error.stillPreparing": "O espaço de trabalho ainda está sendo preparado",
|
||||
"workspace.status.checking": "Verificando alterações não mescladas...",
|
||||
"workspace.status.error": "Não foi possível verificar o status do git.",
|
||||
"workspace.status.clean": "Nenhuma alteração não mesclada detectada.",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
"command.category.theme": "Tema",
|
||||
"command.category.language": "Sprog",
|
||||
"command.category.file": "Fil",
|
||||
"command.category.context": "Kontekst",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Model",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -15,6 +16,7 @@ export const dict = {
|
||||
"command.category.permissions": "Tilladelser",
|
||||
"command.category.workspace": "Arbejdsområde",
|
||||
|
||||
"command.category.settings": "Indstillinger",
|
||||
"theme.scheme.system": "System",
|
||||
"theme.scheme.light": "Lys",
|
||||
"theme.scheme.dark": "Mørk",
|
||||
@@ -23,6 +25,7 @@ export const dict = {
|
||||
"command.project.open": "Åbn projekt",
|
||||
"command.provider.connect": "Tilslut udbyder",
|
||||
"command.server.switch": "Skift server",
|
||||
"command.settings.open": "Åbn indstillinger",
|
||||
"command.session.previous": "Forrige session",
|
||||
"command.session.next": "Næste session",
|
||||
"command.session.archive": "Arkivér session",
|
||||
@@ -40,7 +43,10 @@ export const dict = {
|
||||
"command.session.new": "Ny session",
|
||||
"command.file.open": "Åbn fil",
|
||||
"command.file.open.description": "Søg i filer og kommandoer",
|
||||
"command.context.addSelection": "Tilføj markering til kontekst",
|
||||
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
|
||||
"command.terminal.toggle": "Skift terminal",
|
||||
"command.fileTree.toggle": "Skift filtræ",
|
||||
"command.review.toggle": "Skift gennemgang",
|
||||
"command.terminal.new": "Ny terminal",
|
||||
"command.terminal.new.description": "Opret en ny terminalfane",
|
||||
@@ -117,6 +123,7 @@ export const dict = {
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Med en enkelt API-nøgle får du adgang til modeller som Claude, GPT, Gemini, GLM og flere.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Besøg ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " for at hente din API-nøgle.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Besøg ",
|
||||
"provider.connect.oauth.code.visit.link": "dette link",
|
||||
@@ -134,13 +141,32 @@ export const dict = {
|
||||
"provider.connect.toast.connected.title": "{{provider}} forbundet",
|
||||
"provider.connect.toast.connected.description": "{{provider}} modeller er nu tilgængelige.",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet",
|
||||
"provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke længere tilgængelige.",
|
||||
"model.tag.free": "Gratis",
|
||||
"model.tag.latest": "Nyeste",
|
||||
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
"model.provider.openai": "OpenAI",
|
||||
"model.provider.google": "Google",
|
||||
"model.provider.xai": "xAI",
|
||||
"model.provider.meta": "Meta",
|
||||
"model.input.text": "tekst",
|
||||
"model.input.image": "billede",
|
||||
"model.input.audio": "lyd",
|
||||
"model.input.video": "video",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Tillader: {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "Tillader tænkning",
|
||||
"model.tooltip.reasoning.none": "Ingen tænkning",
|
||||
"model.tooltip.context": "Kontekstgrænse {{limit}}",
|
||||
"common.search.placeholder": "Søg",
|
||||
"common.goBack": "Gå tilbage",
|
||||
"common.loading": "Indlæser",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Annuller",
|
||||
"common.connect": "Forbind",
|
||||
"common.disconnect": "Frakobl",
|
||||
"common.submit": "Indsend",
|
||||
"common.save": "Gem",
|
||||
"common.saving": "Gemmer...",
|
||||
@@ -149,6 +175,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "Indtast shell-kommando...",
|
||||
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
||||
"prompt.placeholder.summarizeComment": "Opsummér kommentar…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc for at afslutte",
|
||||
|
||||
@@ -252,6 +280,9 @@ export const dict = {
|
||||
"dialog.project.edit.color": "Farve",
|
||||
"dialog.project.edit.color.select": "Vælg farven {{color}}",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde",
|
||||
"dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note":
|
||||
'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.',
|
||||
@@ -301,6 +332,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Sprog",
|
||||
"toast.language.description": "Skiftede til {{language}}",
|
||||
@@ -318,6 +350,9 @@ export const dict = {
|
||||
|
||||
"toast.file.loadFailed.title": "Kunne ikke indlæse fil",
|
||||
|
||||
"toast.file.listFailed.title": "Kunne ikke liste filer",
|
||||
"toast.context.noLineSelection.title": "Ingen linjevalg",
|
||||
"toast.context.noLineSelection.description": "Vælg først et linjeinterval i en filfane.",
|
||||
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til udklipsholder",
|
||||
"toast.session.share.success.title": "Session delt",
|
||||
"toast.session.share.success.description": "Delings-URL kopieret til udklipsholder!",
|
||||
@@ -392,13 +427,19 @@ export const dict = {
|
||||
"session.tab.context": "Kontekst",
|
||||
"session.panel.reviewAndFiles": "Gennemgang og filer",
|
||||
"session.review.filesChanged": "{{count}} Filer ændret",
|
||||
"session.review.change.one": "Ændring",
|
||||
"session.review.change.other": "Ændringer",
|
||||
"session.review.loadingChanges": "Indlæser ændringer...",
|
||||
"session.review.empty": "Ingen ændringer i denne session endnu",
|
||||
"session.review.noChanges": "Ingen ændringer",
|
||||
"session.files.selectToOpen": "Vælg en fil at åbne",
|
||||
"session.files.all": "Alle filer",
|
||||
"session.messages.renderEarlier": "Vis tidligere beskeder",
|
||||
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
|
||||
"session.messages.loadEarlier": "Indlæs tidligere beskeder",
|
||||
"session.messages.loading": "Indlæser beskeder...",
|
||||
|
||||
"session.messages.jumpToLatest": "Gå til seneste",
|
||||
"session.context.addToContext": "Tilføj {{selection}} til kontekst",
|
||||
|
||||
"session.new.worktree.main": "Hovedgren",
|
||||
@@ -440,6 +481,8 @@ export const dict = {
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Luk terminal",
|
||||
|
||||
"terminal.connectionLost.title": "Forbindelse mistet",
|
||||
"terminal.connectionLost.description": "Terminalforbindelsen blev afbrudt. Dette kan ske, når serveren genstarter.",
|
||||
"common.closeTab": "Luk fane",
|
||||
"common.dismiss": "Afvis",
|
||||
"common.requestFailed": "Forespørgsel mislykkedes",
|
||||
@@ -453,6 +496,8 @@ export const dict = {
|
||||
"common.edit": "Rediger",
|
||||
"common.loadMore": "Indlæs flere",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "Skift menu",
|
||||
"sidebar.nav.projectsAndSessions": "Projekter og sessioner",
|
||||
"sidebar.settings": "Indstillinger",
|
||||
"sidebar.help": "Hjælp",
|
||||
@@ -464,12 +509,15 @@ export const dict = {
|
||||
"sidebar.project.recentSessions": "Seneste sessioner",
|
||||
"sidebar.project.viewAllSessions": "Vis alle sessioner",
|
||||
|
||||
"app.name.desktop": "OpenCode Desktop",
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.section.server": "Server",
|
||||
"settings.tab.general": "Generelt",
|
||||
"settings.tab.shortcuts": "Genveje",
|
||||
|
||||
"settings.general.section.appearance": "Udseende",
|
||||
"settings.general.section.notifications": "Systemmeddelelser",
|
||||
"settings.general.section.updates": "Opdateringer",
|
||||
"settings.general.section.sounds": "Lydeffekter",
|
||||
|
||||
"settings.general.row.language.title": "Sprog",
|
||||
@@ -481,6 +529,75 @@ export const dict = {
|
||||
"settings.general.row.font.title": "Skrifttype",
|
||||
"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.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": "Alarm 01",
|
||||
"sound.option.alert02": "Alarm 02",
|
||||
"sound.option.alert03": "Alarm 03",
|
||||
"sound.option.alert04": "Alarm 04",
|
||||
"sound.option.alert05": "Alarm 05",
|
||||
"sound.option.alert06": "Alarm 06",
|
||||
"sound.option.alert07": "Alarm 07",
|
||||
"sound.option.alert08": "Alarm 08",
|
||||
"sound.option.alert09": "Alarm 09",
|
||||
"sound.option.alert10": "Alarm 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": "Nej 01",
|
||||
"sound.option.nope02": "Nej 02",
|
||||
"sound.option.nope03": "Nej 03",
|
||||
"sound.option.nope04": "Nej 04",
|
||||
"sound.option.nope05": "Nej 05",
|
||||
"sound.option.nope06": "Nej 06",
|
||||
"sound.option.nope07": "Nej 07",
|
||||
"sound.option.nope08": "Nej 08",
|
||||
"sound.option.nope09": "Nej 09",
|
||||
"sound.option.nope10": "Nej 10",
|
||||
"sound.option.nope11": "Nej 11",
|
||||
"sound.option.nope12": "Nej 12",
|
||||
"sound.option.yup01": "Ja 01",
|
||||
"sound.option.yup02": "Ja 02",
|
||||
"sound.option.yup03": "Ja 03",
|
||||
"sound.option.yup04": "Ja 04",
|
||||
"sound.option.yup05": "Ja 05",
|
||||
"sound.option.yup06": "Ja 06",
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Vis systemmeddelelse når agenten er færdig eller kræver opmærksomhed",
|
||||
@@ -516,6 +633,13 @@ export const dict = {
|
||||
|
||||
"settings.providers.title": "Udbydere",
|
||||
"settings.providers.description": "Udbyderindstillinger vil kunne konfigureres her.",
|
||||
"settings.providers.section.connected": "Forbundne udbydere",
|
||||
"settings.providers.connected.empty": "Ingen forbundne udbydere",
|
||||
"settings.providers.section.popular": "Populære udbydere",
|
||||
"settings.providers.tag.environment": "Miljø",
|
||||
"settings.providers.tag.config": "Konfiguration",
|
||||
"settings.providers.tag.custom": "Brugerdefineret",
|
||||
"settings.providers.tag.other": "Andet",
|
||||
"settings.models.title": "Modeller",
|
||||
"settings.models.description": "Modelindstillinger vil kunne konfigureres her.",
|
||||
"settings.agents.title": "Agenter",
|
||||
@@ -583,6 +707,7 @@ export const dict = {
|
||||
"workspace.reset.failed.title": "Kunne ikke nulstille arbejdsområde",
|
||||
"workspace.reset.success.title": "Arbejdsområde nulstillet",
|
||||
"workspace.reset.success.description": "Arbejdsområdet matcher nu hovedgrenen.",
|
||||
"workspace.error.stillPreparing": "Arbejdsområdet er stadig ved at blive klargjort",
|
||||
"workspace.status.checking": "Tjekker for uflettede ændringer...",
|
||||
"workspace.status.error": "Kunne ikke bekræfte git-status.",
|
||||
"workspace.status.clean": "Ingen uflettede ændringer fundet.",
|
||||
|
||||
@@ -12,6 +12,7 @@ export const dict = {
|
||||
"command.category.theme": "Thema",
|
||||
"command.category.language": "Sprache",
|
||||
"command.category.file": "Datei",
|
||||
"command.category.context": "Kontext",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modell",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -19,6 +20,7 @@ export const dict = {
|
||||
"command.category.permissions": "Berechtigungen",
|
||||
"command.category.workspace": "Arbeitsbereich",
|
||||
|
||||
"command.category.settings": "Einstellungen",
|
||||
"theme.scheme.system": "System",
|
||||
"theme.scheme.light": "Hell",
|
||||
"theme.scheme.dark": "Dunkel",
|
||||
@@ -27,6 +29,7 @@ export const dict = {
|
||||
"command.project.open": "Projekt öffnen",
|
||||
"command.provider.connect": "Anbieter verbinden",
|
||||
"command.server.switch": "Server wechseln",
|
||||
"command.settings.open": "Einstellungen öffnen",
|
||||
"command.session.previous": "Vorherige Sitzung",
|
||||
"command.session.next": "Nächste Sitzung",
|
||||
"command.session.archive": "Sitzung archivieren",
|
||||
@@ -44,7 +47,10 @@ export const dict = {
|
||||
"command.session.new": "Neue Sitzung",
|
||||
"command.file.open": "Datei öffnen",
|
||||
"command.file.open.description": "Dateien und Befehle durchsuchen",
|
||||
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
|
||||
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
|
||||
"command.terminal.toggle": "Terminal umschalten",
|
||||
"command.fileTree.toggle": "Dateibaum umschalten",
|
||||
"command.review.toggle": "Überprüfung umschalten",
|
||||
"command.terminal.new": "Neues Terminal",
|
||||
"command.terminal.new.description": "Neuen Terminal-Tab erstellen",
|
||||
@@ -121,6 +127,7 @@ export const dict = {
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Mit einem einzigen API-Schlüssel erhalten Sie Zugriff auf Modelle wie Claude, GPT, Gemini, GLM und mehr.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Besuchen Sie ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": ", um Ihren API-Schlüssel zu erhalten.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Besuchen Sie ",
|
||||
"provider.connect.oauth.code.visit.link": "diesen Link",
|
||||
@@ -138,13 +145,32 @@ export const dict = {
|
||||
"provider.connect.toast.connected.title": "{{provider}} verbunden",
|
||||
"provider.connect.toast.connected.description": "{{provider}} Modelle sind jetzt verfügbar.",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} getrennt",
|
||||
"provider.disconnect.toast.disconnected.description": "Die {{provider}}-Modelle sind nicht mehr verfügbar.",
|
||||
"model.tag.free": "Kostenlos",
|
||||
"model.tag.latest": "Neueste",
|
||||
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
"model.provider.openai": "OpenAI",
|
||||
"model.provider.google": "Google",
|
||||
"model.provider.xai": "xAI",
|
||||
"model.provider.meta": "Meta",
|
||||
"model.input.text": "Text",
|
||||
"model.input.image": "Bild",
|
||||
"model.input.audio": "Audio",
|
||||
"model.input.video": "Video",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Erlaubt: {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "Erlaubt Reasoning",
|
||||
"model.tooltip.reasoning.none": "Kein Reasoning",
|
||||
"model.tooltip.context": "Kontextlimit {{limit}}",
|
||||
"common.search.placeholder": "Suchen",
|
||||
"common.goBack": "Zurück",
|
||||
"common.loading": "Laden",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.connect": "Verbinden",
|
||||
"common.disconnect": "Trennen",
|
||||
"common.submit": "Absenden",
|
||||
"common.save": "Speichern",
|
||||
"common.saving": "Speichert...",
|
||||
@@ -153,6 +179,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "Shell-Befehl eingeben...",
|
||||
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
||||
"prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc zum Verlassen",
|
||||
|
||||
@@ -257,6 +285,10 @@ export const dict = {
|
||||
"dialog.project.edit.color": "Farbe",
|
||||
"dialog.project.edit.color.select": "{{color}}-Farbe auswählen",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "Startup-Skript für Arbeitsbereich",
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "z. B. bun install",
|
||||
"context.breakdown.title": "Kontext-Aufschlüsselung",
|
||||
"context.breakdown.note":
|
||||
'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.',
|
||||
@@ -306,6 +338,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Sprache",
|
||||
"toast.language.description": "Zu {{language}} gewechselt",
|
||||
@@ -323,6 +356,9 @@ export const dict = {
|
||||
|
||||
"toast.file.loadFailed.title": "Datei konnte nicht geladen werden",
|
||||
|
||||
"toast.file.listFailed.title": "Dateien konnten nicht aufgelistet werden",
|
||||
"toast.context.noLineSelection.title": "Keine Zeilenauswahl",
|
||||
"toast.context.noLineSelection.description": "Wählen Sie zuerst einen Zeilenbereich in einem Datei-Tab aus.",
|
||||
"toast.session.share.copyFailed.title": "URL konnte nicht in die Zwischenablage kopiert werden",
|
||||
"toast.session.share.success.title": "Sitzung geteilt",
|
||||
"toast.session.share.success.description": "Teilen-URL in die Zwischenablage kopiert!",
|
||||
@@ -399,13 +435,19 @@ export const dict = {
|
||||
"session.tab.context": "Kontext",
|
||||
"session.panel.reviewAndFiles": "Überprüfung und Dateien",
|
||||
"session.review.filesChanged": "{{count}} Dateien geändert",
|
||||
"session.review.change.one": "Änderung",
|
||||
"session.review.change.other": "Änderungen",
|
||||
"session.review.loadingChanges": "Lade Änderungen...",
|
||||
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
|
||||
"session.review.noChanges": "Keine Änderungen",
|
||||
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
|
||||
"session.files.all": "Alle Dateien",
|
||||
"session.messages.renderEarlier": "Frühere Nachrichten rendern",
|
||||
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
|
||||
"session.messages.loadEarlier": "Frühere Nachrichten laden",
|
||||
"session.messages.loading": "Lade Nachrichten...",
|
||||
|
||||
"session.messages.jumpToLatest": "Zum neuesten springen",
|
||||
"session.context.addToContext": "{{selection}} zum Kontext hinzufügen",
|
||||
|
||||
"session.new.worktree.main": "Haupt-Branch",
|
||||
@@ -447,6 +489,9 @@ export const dict = {
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Terminal schließen",
|
||||
|
||||
"terminal.connectionLost.title": "Verbindung verloren",
|
||||
"terminal.connectionLost.description":
|
||||
"Die Terminalverbindung wurde unterbrochen. Das kann passieren, wenn der Server neu startet.",
|
||||
"common.closeTab": "Tab schließen",
|
||||
"common.dismiss": "Verwerfen",
|
||||
"common.requestFailed": "Anfrage fehlgeschlagen",
|
||||
@@ -460,6 +505,8 @@ export const dict = {
|
||||
"common.edit": "Bearbeiten",
|
||||
"common.loadMore": "Mehr laden",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "Menü umschalten",
|
||||
"sidebar.nav.projectsAndSessions": "Projekte und Sitzungen",
|
||||
"sidebar.settings": "Einstellungen",
|
||||
"sidebar.help": "Hilfe",
|
||||
@@ -472,12 +519,15 @@ export const dict = {
|
||||
"sidebar.project.recentSessions": "Letzte Sitzungen",
|
||||
"sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen",
|
||||
|
||||
"app.name.desktop": "OpenCode Desktop",
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.section.server": "Server",
|
||||
"settings.tab.general": "Allgemein",
|
||||
"settings.tab.shortcuts": "Tastenkombinationen",
|
||||
|
||||
"settings.general.section.appearance": "Erscheinungsbild",
|
||||
"settings.general.section.notifications": "Systembenachrichtigungen",
|
||||
"settings.general.section.updates": "Updates",
|
||||
"settings.general.section.sounds": "Soundeffekte",
|
||||
|
||||
"settings.general.row.language.title": "Sprache",
|
||||
@@ -489,6 +539,75 @@ export const dict = {
|
||||
"settings.general.row.font.title": "Schriftart",
|
||||
"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.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": "Alarm 01",
|
||||
"sound.option.alert02": "Alarm 02",
|
||||
"sound.option.alert03": "Alarm 03",
|
||||
"sound.option.alert04": "Alarm 04",
|
||||
"sound.option.alert05": "Alarm 05",
|
||||
"sound.option.alert06": "Alarm 06",
|
||||
"sound.option.alert07": "Alarm 07",
|
||||
"sound.option.alert08": "Alarm 08",
|
||||
"sound.option.alert09": "Alarm 09",
|
||||
"sound.option.alert10": "Alarm 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": "Nein 01",
|
||||
"sound.option.nope02": "Nein 02",
|
||||
"sound.option.nope03": "Nein 03",
|
||||
"sound.option.nope04": "Nein 04",
|
||||
"sound.option.nope05": "Nein 05",
|
||||
"sound.option.nope06": "Nein 06",
|
||||
"sound.option.nope07": "Nein 07",
|
||||
"sound.option.nope08": "Nein 08",
|
||||
"sound.option.nope09": "Nein 09",
|
||||
"sound.option.nope10": "Nein 10",
|
||||
"sound.option.nope11": "Nein 11",
|
||||
"sound.option.nope12": "Nein 12",
|
||||
"sound.option.yup01": "Ja 01",
|
||||
"sound.option.yup02": "Ja 02",
|
||||
"sound.option.yup03": "Ja 03",
|
||||
"sound.option.yup04": "Ja 04",
|
||||
"sound.option.yup05": "Ja 05",
|
||||
"sound.option.yup06": "Ja 06",
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Systembenachrichtigung anzeigen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt",
|
||||
@@ -525,6 +644,13 @@ export const dict = {
|
||||
|
||||
"settings.providers.title": "Anbieter",
|
||||
"settings.providers.description": "Anbietereinstellungen können hier konfiguriert werden.",
|
||||
"settings.providers.section.connected": "Verbundene Anbieter",
|
||||
"settings.providers.connected.empty": "Keine verbundenen Anbieter",
|
||||
"settings.providers.section.popular": "Beliebte Anbieter",
|
||||
"settings.providers.tag.environment": "Umgebung",
|
||||
"settings.providers.tag.config": "Konfiguration",
|
||||
"settings.providers.tag.custom": "Benutzerdefiniert",
|
||||
"settings.providers.tag.other": "Andere",
|
||||
"settings.models.title": "Modelle",
|
||||
"settings.models.description": "Modelleinstellungen können hier konfiguriert werden.",
|
||||
"settings.agents.title": "Agenten",
|
||||
@@ -592,6 +718,7 @@ export const dict = {
|
||||
"workspace.reset.failed.title": "Arbeitsbereich konnte nicht zurückgesetzt werden",
|
||||
"workspace.reset.success.title": "Arbeitsbereich zurückgesetzt",
|
||||
"workspace.reset.success.description": "Der Arbeitsbereich entspricht jetzt dem Standard-Branch.",
|
||||
"workspace.error.stillPreparing": "Arbeitsbereich wird noch vorbereitet",
|
||||
"workspace.status.checking": "Suche nach nicht zusammengeführten Änderungen...",
|
||||
"workspace.status.error": "Git-Status konnte nicht überprüft werden.",
|
||||
"workspace.status.clean": "Keine nicht zusammengeführten Änderungen erkannt.",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
"command.category.theme": "Theme",
|
||||
"command.category.language": "Language",
|
||||
"command.category.file": "File",
|
||||
"command.category.context": "Context",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Model",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -42,7 +43,10 @@ export const dict = {
|
||||
"command.session.new": "New session",
|
||||
"command.file.open": "Open file",
|
||||
"command.file.open.description": "Search files and commands",
|
||||
"command.context.addSelection": "Add selection to context",
|
||||
"command.context.addSelection.description": "Add selected lines from the current file",
|
||||
"command.terminal.toggle": "Toggle terminal",
|
||||
"command.fileTree.toggle": "Toggle file tree",
|
||||
"command.review.toggle": "Toggle review",
|
||||
"command.terminal.new": "New terminal",
|
||||
"command.terminal.new.description": "Create a new terminal tab",
|
||||
@@ -87,9 +91,13 @@ export const dict = {
|
||||
"dialog.provider.group.popular": "Popular",
|
||||
"dialog.provider.group.other": "Other",
|
||||
"dialog.provider.tag.recommended": "Recommended",
|
||||
"dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key",
|
||||
"dialog.provider.openai.note": "Connect with ChatGPT Pro/Plus or API key",
|
||||
"dialog.provider.copilot.note": "Connect with Copilot or API key",
|
||||
"dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more",
|
||||
"dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max",
|
||||
"dialog.provider.copilot.note": "Claude models for coding assistance",
|
||||
"dialog.provider.openai.note": "GPT models for fast, capable general AI tasks",
|
||||
"dialog.provider.google.note": "Gemini models for fast, structured responses",
|
||||
"dialog.provider.openrouter.note": "Access all supported models from one provider",
|
||||
"dialog.provider.vercel.note": "Unified access to AI models with smart routing",
|
||||
|
||||
"dialog.model.select.title": "Select model",
|
||||
"dialog.model.search.placeholder": "Search models",
|
||||
@@ -158,7 +166,8 @@ export const dict = {
|
||||
"model.tooltip.context": "Context limit {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Search",
|
||||
"common.goBack": "Go back",
|
||||
"common.goBack": "Back",
|
||||
"common.goForward": "Forward",
|
||||
"common.loading": "Loading",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Cancel",
|
||||
@@ -172,6 +181,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "Enter shell command...",
|
||||
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
||||
"prompt.placeholder.summarizeComment": "Summarize comment…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc to exit",
|
||||
|
||||
@@ -326,6 +337,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Language",
|
||||
"toast.language.description": "Switched to {{language}}",
|
||||
@@ -342,6 +354,10 @@ export const dict = {
|
||||
"toast.model.none.description": "Connect a provider to summarize this session",
|
||||
|
||||
"toast.file.loadFailed.title": "Failed to load file",
|
||||
"toast.file.listFailed.title": "Failed to list files",
|
||||
|
||||
"toast.context.noLineSelection.title": "No line selection",
|
||||
"toast.context.noLineSelection.description": "Select a line range in a file tab first.",
|
||||
|
||||
"toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
|
||||
"toast.session.share.success.title": "Session shared",
|
||||
@@ -417,8 +433,15 @@ export const dict = {
|
||||
"session.tab.context": "Context",
|
||||
"session.panel.reviewAndFiles": "Review and files",
|
||||
"session.review.filesChanged": "{{count}} Files Changed",
|
||||
"session.review.change.one": "Change",
|
||||
"session.review.change.other": "Changes",
|
||||
"session.review.loadingChanges": "Loading changes...",
|
||||
"session.review.empty": "No changes in this session yet",
|
||||
"session.review.noChanges": "No changes",
|
||||
|
||||
"session.files.selectToOpen": "Select a file to open",
|
||||
"session.files.all": "All files",
|
||||
|
||||
"session.messages.renderEarlier": "Render earlier messages",
|
||||
"session.messages.loadingEarlier": "Loading earlier messages...",
|
||||
"session.messages.loadEarlier": "Load earlier messages",
|
||||
@@ -495,6 +518,8 @@ export const dict = {
|
||||
"sidebar.project.recentSessions": "Recent sessions",
|
||||
"sidebar.project.viewAllSessions": "View all sessions",
|
||||
|
||||
"app.name.desktop": "OpenCode Desktop",
|
||||
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.section.server": "Server",
|
||||
"settings.tab.general": "General",
|
||||
@@ -502,6 +527,7 @@ export const dict = {
|
||||
|
||||
"settings.general.section.appearance": "Appearance",
|
||||
"settings.general.section.notifications": "System notifications",
|
||||
"settings.general.section.updates": "Updates",
|
||||
"settings.general.section.sounds": "Sound effects",
|
||||
|
||||
"settings.general.row.language.title": "Language",
|
||||
@@ -512,6 +538,18 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
||||
"settings.general.row.font.title": "Font",
|
||||
"settings.general.row.font.description": "Customise the mono font used in code blocks",
|
||||
|
||||
"settings.general.row.releaseNotes.title": "Release notes",
|
||||
"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.cascadiaCode": "Cascadia Code",
|
||||
"font.option.firaCode": "Fira Code",
|
||||
@@ -678,6 +716,7 @@ export const dict = {
|
||||
"workspace.reset.failed.title": "Failed to reset workspace",
|
||||
"workspace.reset.success.title": "Workspace reset",
|
||||
"workspace.reset.success.description": "Workspace now matches the default branch.",
|
||||
"workspace.error.stillPreparing": "Workspace is still preparing",
|
||||
"workspace.status.checking": "Checking for unmerged changes...",
|
||||
"workspace.status.error": "Unable to verify git status.",
|
||||
"workspace.status.clean": "No unmerged changes detected.",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
"command.category.theme": "Tema",
|
||||
"command.category.language": "Idioma",
|
||||
"command.category.file": "Archivo",
|
||||
"command.category.context": "Contexto",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modelo",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -15,6 +16,7 @@ export const dict = {
|
||||
"command.category.permissions": "Permisos",
|
||||
"command.category.workspace": "Espacio de trabajo",
|
||||
|
||||
"command.category.settings": "Ajustes",
|
||||
"theme.scheme.system": "Sistema",
|
||||
"theme.scheme.light": "Claro",
|
||||
"theme.scheme.dark": "Oscuro",
|
||||
@@ -23,6 +25,7 @@ export const dict = {
|
||||
"command.project.open": "Abrir proyecto",
|
||||
"command.provider.connect": "Conectar proveedor",
|
||||
"command.server.switch": "Cambiar servidor",
|
||||
"command.settings.open": "Abrir ajustes",
|
||||
"command.session.previous": "Sesión anterior",
|
||||
"command.session.next": "Siguiente sesión",
|
||||
"command.session.archive": "Archivar sesión",
|
||||
@@ -40,7 +43,10 @@ export const dict = {
|
||||
"command.session.new": "Nueva sesión",
|
||||
"command.file.open": "Abrir archivo",
|
||||
"command.file.open.description": "Buscar archivos y comandos",
|
||||
"command.context.addSelection": "Añadir selección al contexto",
|
||||
"command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual",
|
||||
"command.terminal.toggle": "Alternar terminal",
|
||||
"command.fileTree.toggle": "Alternar árbol de archivos",
|
||||
"command.review.toggle": "Alternar revisión",
|
||||
"command.terminal.new": "Nueva terminal",
|
||||
"command.terminal.new.description": "Crear una nueva pestaña de terminal",
|
||||
@@ -117,6 +123,7 @@ export const dict = {
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Con una sola clave API obtendrás acceso a modelos como Claude, GPT, Gemini, GLM y más.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Visita ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " para obtener tu clave API.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Visita ",
|
||||
"provider.connect.oauth.code.visit.link": "este enlace",
|
||||
@@ -134,13 +141,32 @@ export const dict = {
|
||||
"provider.connect.toast.connected.title": "{{provider}} conectado",
|
||||
"provider.connect.toast.connected.description": "Los modelos de {{provider}} ahora están disponibles para usar.",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} desconectado",
|
||||
"provider.disconnect.toast.disconnected.description": "Los modelos de {{provider}} ya no están disponibles.",
|
||||
"model.tag.free": "Gratis",
|
||||
"model.tag.latest": "Último",
|
||||
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
"model.provider.openai": "OpenAI",
|
||||
"model.provider.google": "Google",
|
||||
"model.provider.xai": "xAI",
|
||||
"model.provider.meta": "Meta",
|
||||
"model.input.text": "texto",
|
||||
"model.input.image": "imagen",
|
||||
"model.input.audio": "audio",
|
||||
"model.input.video": "video",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Permite: {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "Permite razonamiento",
|
||||
"model.tooltip.reasoning.none": "Sin razonamiento",
|
||||
"model.tooltip.context": "Límite de contexto {{limit}}",
|
||||
"common.search.placeholder": "Buscar",
|
||||
"common.goBack": "Volver",
|
||||
"common.loading": "Cargando",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.connect": "Conectar",
|
||||
"common.disconnect": "Desconectar",
|
||||
"common.submit": "Enviar",
|
||||
"common.save": "Guardar",
|
||||
"common.saving": "Guardando...",
|
||||
@@ -149,6 +175,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "Introduce comando de shell...",
|
||||
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
||||
"prompt.placeholder.summarizeComment": "Resumir comentario…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc para salir",
|
||||
|
||||
@@ -252,6 +280,10 @@ export const dict = {
|
||||
"dialog.project.edit.color": "Color",
|
||||
"dialog.project.edit.color.select": "Seleccionar color {{color}}",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "Script de inicio del espacio de trabajo",
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install",
|
||||
"context.breakdown.title": "Desglose de Contexto",
|
||||
"context.breakdown.note":
|
||||
'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.',
|
||||
@@ -301,6 +333,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Idioma",
|
||||
"toast.language.description": "Cambiado a {{language}}",
|
||||
@@ -318,6 +351,9 @@ export const dict = {
|
||||
|
||||
"toast.file.loadFailed.title": "Fallo al cargar archivo",
|
||||
|
||||
"toast.file.listFailed.title": "Fallo al listar archivos",
|
||||
"toast.context.noLineSelection.title": "Sin selección de líneas",
|
||||
"toast.context.noLineSelection.description": "Primero selecciona un rango de líneas en una pestaña de archivo.",
|
||||
"toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles",
|
||||
"toast.session.share.success.title": "Sesión compartida",
|
||||
"toast.session.share.success.description": "¡URL compartida copiada al portapapeles!",
|
||||
@@ -393,13 +429,19 @@ export const dict = {
|
||||
"session.tab.context": "Contexto",
|
||||
"session.panel.reviewAndFiles": "Revisión y archivos",
|
||||
"session.review.filesChanged": "{{count}} Archivos Cambiados",
|
||||
"session.review.change.one": "Cambio",
|
||||
"session.review.change.other": "Cambios",
|
||||
"session.review.loadingChanges": "Cargando cambios...",
|
||||
"session.review.empty": "No hay cambios en esta sesión aún",
|
||||
"session.review.noChanges": "Sin cambios",
|
||||
"session.files.selectToOpen": "Selecciona un archivo para abrir",
|
||||
"session.files.all": "Todos los archivos",
|
||||
"session.messages.renderEarlier": "Renderizar mensajes anteriores",
|
||||
"session.messages.loadingEarlier": "Cargando mensajes anteriores...",
|
||||
"session.messages.loadEarlier": "Cargar mensajes anteriores",
|
||||
"session.messages.loading": "Cargando mensajes...",
|
||||
|
||||
"session.messages.jumpToLatest": "Ir al último",
|
||||
"session.context.addToContext": "Añadir {{selection}} al contexto",
|
||||
|
||||
"session.new.worktree.main": "Rama principal",
|
||||
@@ -441,6 +483,9 @@ export const dict = {
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Cerrar terminal",
|
||||
|
||||
"terminal.connectionLost.title": "Conexión perdida",
|
||||
"terminal.connectionLost.description":
|
||||
"La conexión del terminal se interrumpió. Esto puede ocurrir cuando el servidor se reinicia.",
|
||||
"common.closeTab": "Cerrar pestaña",
|
||||
"common.dismiss": "Descartar",
|
||||
"common.requestFailed": "Solicitud fallida",
|
||||
@@ -454,6 +499,8 @@ export const dict = {
|
||||
"common.edit": "Editar",
|
||||
"common.loadMore": "Cargar más",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "Alternar menú",
|
||||
"sidebar.nav.projectsAndSessions": "Proyectos y sesiones",
|
||||
"sidebar.settings": "Ajustes",
|
||||
"sidebar.help": "Ayuda",
|
||||
@@ -465,12 +512,15 @@ export const dict = {
|
||||
"sidebar.project.recentSessions": "Sesiones recientes",
|
||||
"sidebar.project.viewAllSessions": "Ver todas las sesiones",
|
||||
|
||||
"app.name.desktop": "OpenCode Desktop",
|
||||
"settings.section.desktop": "Escritorio",
|
||||
"settings.section.server": "Servidor",
|
||||
"settings.tab.general": "General",
|
||||
"settings.tab.shortcuts": "Atajos",
|
||||
|
||||
"settings.general.section.appearance": "Apariencia",
|
||||
"settings.general.section.notifications": "Notificaciones del sistema",
|
||||
"settings.general.section.updates": "Actualizaciones",
|
||||
"settings.general.section.sounds": "Efectos de sonido",
|
||||
|
||||
"settings.general.row.language.title": "Idioma",
|
||||
@@ -482,6 +532,76 @@ export const dict = {
|
||||
"settings.general.row.font.title": "Fuente",
|
||||
"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.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": "Alerta 01",
|
||||
"sound.option.alert02": "Alerta 02",
|
||||
"sound.option.alert03": "Alerta 03",
|
||||
"sound.option.alert04": "Alerta 04",
|
||||
"sound.option.alert05": "Alerta 05",
|
||||
"sound.option.alert06": "Alerta 06",
|
||||
"sound.option.alert07": "Alerta 07",
|
||||
"sound.option.alert08": "Alerta 08",
|
||||
"sound.option.alert09": "Alerta 09",
|
||||
"sound.option.alert10": "Alerta 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": "No 01",
|
||||
"sound.option.nope02": "No 02",
|
||||
"sound.option.nope03": "No 03",
|
||||
"sound.option.nope04": "No 04",
|
||||
"sound.option.nope05": "No 05",
|
||||
"sound.option.nope06": "No 06",
|
||||
"sound.option.nope07": "No 07",
|
||||
"sound.option.nope08": "No 08",
|
||||
"sound.option.nope09": "No 09",
|
||||
"sound.option.nope10": "No 10",
|
||||
"sound.option.nope11": "No 11",
|
||||
"sound.option.nope12": "No 12",
|
||||
"sound.option.yup01": "Sí 01",
|
||||
"sound.option.yup02": "Sí 02",
|
||||
"sound.option.yup03": "Sí 03",
|
||||
"sound.option.yup04": "Sí 04",
|
||||
"sound.option.yup05": "Sí 05",
|
||||
"sound.option.yup06": "Sí 06",
|
||||
"settings.general.notifications.agent.title": "Agente",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Mostrar notificación del sistema cuando el agente termine o necesite atención",
|
||||
@@ -519,6 +639,13 @@ export const dict = {
|
||||
|
||||
"settings.providers.title": "Proveedores",
|
||||
"settings.providers.description": "La configuración de proveedores estará disponible aquí.",
|
||||
"settings.providers.section.connected": "Proveedores conectados",
|
||||
"settings.providers.connected.empty": "No hay proveedores conectados",
|
||||
"settings.providers.section.popular": "Proveedores populares",
|
||||
"settings.providers.tag.environment": "Entorno",
|
||||
"settings.providers.tag.config": "Configuración",
|
||||
"settings.providers.tag.custom": "Personalizado",
|
||||
"settings.providers.tag.other": "Otro",
|
||||
"settings.models.title": "Modelos",
|
||||
"settings.models.description": "La configuración de modelos estará disponible aquí.",
|
||||
"settings.agents.title": "Agentes",
|
||||
@@ -586,6 +713,7 @@ export const dict = {
|
||||
"workspace.reset.failed.title": "Fallo al restablecer espacio de trabajo",
|
||||
"workspace.reset.success.title": "Espacio de trabajo restablecido",
|
||||
"workspace.reset.success.description": "El espacio de trabajo ahora coincide con la rama predeterminada.",
|
||||
"workspace.error.stillPreparing": "El espacio de trabajo aún se está preparando",
|
||||
"workspace.status.checking": "Comprobando cambios no fusionados...",
|
||||
"workspace.status.error": "No se pudo verificar el estado de git.",
|
||||
"workspace.status.clean": "No se detectaron cambios no fusionados.",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
"command.category.theme": "Thème",
|
||||
"command.category.language": "Langue",
|
||||
"command.category.file": "Fichier",
|
||||
"command.category.context": "Contexte",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modèle",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -15,6 +16,7 @@ export const dict = {
|
||||
"command.category.permissions": "Permissions",
|
||||
"command.category.workspace": "Espace de travail",
|
||||
|
||||
"command.category.settings": "Paramètres",
|
||||
"theme.scheme.system": "Système",
|
||||
"theme.scheme.light": "Clair",
|
||||
"theme.scheme.dark": "Sombre",
|
||||
@@ -23,6 +25,7 @@ export const dict = {
|
||||
"command.project.open": "Ouvrir un projet",
|
||||
"command.provider.connect": "Connecter un fournisseur",
|
||||
"command.server.switch": "Changer de serveur",
|
||||
"command.settings.open": "Ouvrir les paramètres",
|
||||
"command.session.previous": "Session précédente",
|
||||
"command.session.next": "Session suivante",
|
||||
"command.session.archive": "Archiver la session",
|
||||
@@ -40,7 +43,10 @@ export const dict = {
|
||||
"command.session.new": "Nouvelle session",
|
||||
"command.file.open": "Ouvrir un fichier",
|
||||
"command.file.open.description": "Rechercher des fichiers et des commandes",
|
||||
"command.context.addSelection": "Ajouter la sélection au contexte",
|
||||
"command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel",
|
||||
"command.terminal.toggle": "Basculer le terminal",
|
||||
"command.fileTree.toggle": "Basculer l'arborescence des fichiers",
|
||||
"command.review.toggle": "Basculer la revue",
|
||||
"command.terminal.new": "Nouveau terminal",
|
||||
"command.terminal.new.description": "Créer un nouvel onglet de terminal",
|
||||
@@ -117,6 +123,7 @@ export const dict = {
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Avec une seule clé API, vous aurez accès à des modèles tels que Claude, GPT, Gemini, GLM et plus encore.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Visitez ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " pour récupérer votre clé API.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Visitez ",
|
||||
"provider.connect.oauth.code.visit.link": "ce lien",
|
||||
@@ -134,13 +141,32 @@ export const dict = {
|
||||
"provider.connect.toast.connected.title": "{{provider}} connecté",
|
||||
"provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} déconnecté",
|
||||
"provider.disconnect.toast.disconnected.description": "Les modèles {{provider}} ne sont plus disponibles.",
|
||||
"model.tag.free": "Gratuit",
|
||||
"model.tag.latest": "Dernier",
|
||||
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
"model.provider.openai": "OpenAI",
|
||||
"model.provider.google": "Google",
|
||||
"model.provider.xai": "xAI",
|
||||
"model.provider.meta": "Meta",
|
||||
"model.input.text": "texte",
|
||||
"model.input.image": "image",
|
||||
"model.input.audio": "audio",
|
||||
"model.input.video": "vidéo",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Autorise : {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "Autorise le raisonnement",
|
||||
"model.tooltip.reasoning.none": "Sans raisonnement",
|
||||
"model.tooltip.context": "Limite de contexte {{limit}}",
|
||||
"common.search.placeholder": "Rechercher",
|
||||
"common.goBack": "Retour",
|
||||
"common.loading": "Chargement",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Annuler",
|
||||
"common.connect": "Connecter",
|
||||
"common.disconnect": "Déconnecter",
|
||||
"common.submit": "Soumettre",
|
||||
"common.save": "Enregistrer",
|
||||
"common.saving": "Enregistrement...",
|
||||
@@ -149,6 +175,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "Entrez une commande shell...",
|
||||
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
||||
"prompt.placeholder.summarizeComment": "Résumer le commentaire…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc pour quitter",
|
||||
|
||||
@@ -252,6 +280,10 @@ export const dict = {
|
||||
"dialog.project.edit.color": "Couleur",
|
||||
"dialog.project.edit.color.select": "Sélectionner la couleur {{color}}",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "Script de démarrage de l'espace de travail",
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"S'exécute après la création d'un nouvel espace de travail (arbre de travail).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install",
|
||||
"context.breakdown.title": "Répartition du contexte",
|
||||
"context.breakdown.note":
|
||||
"Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les frais généraux.",
|
||||
@@ -301,6 +333,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Langue",
|
||||
"toast.language.description": "Passé à {{language}}",
|
||||
@@ -320,6 +353,9 @@ export const dict = {
|
||||
|
||||
"toast.file.loadFailed.title": "Échec du chargement du fichier",
|
||||
|
||||
"toast.file.listFailed.title": "Échec de la liste des fichiers",
|
||||
"toast.context.noLineSelection.title": "Aucune sélection de lignes",
|
||||
"toast.context.noLineSelection.description": "Sélectionnez d'abord une plage de lignes dans un onglet de fichier.",
|
||||
"toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers",
|
||||
"toast.session.share.success.title": "Session partagée",
|
||||
"toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !",
|
||||
@@ -398,13 +434,19 @@ export const dict = {
|
||||
"session.tab.context": "Contexte",
|
||||
"session.panel.reviewAndFiles": "Revue et fichiers",
|
||||
"session.review.filesChanged": "{{count}} fichiers modifiés",
|
||||
"session.review.change.one": "Modification",
|
||||
"session.review.change.other": "Modifications",
|
||||
"session.review.loadingChanges": "Chargement des modifications...",
|
||||
"session.review.empty": "Aucune modification dans cette session pour l'instant",
|
||||
"session.review.noChanges": "Aucune modification",
|
||||
"session.files.selectToOpen": "Sélectionnez un fichier à ouvrir",
|
||||
"session.files.all": "Tous les fichiers",
|
||||
"session.messages.renderEarlier": "Afficher les messages précédents",
|
||||
"session.messages.loadingEarlier": "Chargement des messages précédents...",
|
||||
"session.messages.loadEarlier": "Charger les messages précédents",
|
||||
"session.messages.loading": "Chargement des messages...",
|
||||
|
||||
"session.messages.jumpToLatest": "Aller au dernier",
|
||||
"session.context.addToContext": "Ajouter {{selection}} au contexte",
|
||||
|
||||
"session.new.worktree.main": "Branche principale",
|
||||
@@ -446,6 +488,9 @@ export const dict = {
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Fermer le terminal",
|
||||
|
||||
"terminal.connectionLost.title": "Connexion perdue",
|
||||
"terminal.connectionLost.description":
|
||||
"La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.",
|
||||
"common.closeTab": "Fermer l'onglet",
|
||||
"common.dismiss": "Ignorer",
|
||||
"common.requestFailed": "La demande a échoué",
|
||||
@@ -459,6 +504,8 @@ export const dict = {
|
||||
"common.edit": "Modifier",
|
||||
"common.loadMore": "Charger plus",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "Basculer le menu",
|
||||
"sidebar.nav.projectsAndSessions": "Projets et sessions",
|
||||
"sidebar.settings": "Paramètres",
|
||||
"sidebar.help": "Aide",
|
||||
@@ -472,12 +519,15 @@ export const dict = {
|
||||
"sidebar.project.recentSessions": "Sessions récentes",
|
||||
"sidebar.project.viewAllSessions": "Voir toutes les sessions",
|
||||
|
||||
"app.name.desktop": "OpenCode Desktop",
|
||||
"settings.section.desktop": "Bureau",
|
||||
"settings.section.server": "Serveur",
|
||||
"settings.tab.general": "Général",
|
||||
"settings.tab.shortcuts": "Raccourcis",
|
||||
|
||||
"settings.general.section.appearance": "Apparence",
|
||||
"settings.general.section.notifications": "Notifications système",
|
||||
"settings.general.section.updates": "Mises à jour",
|
||||
"settings.general.section.sounds": "Effets sonores",
|
||||
|
||||
"settings.general.row.language.title": "Langue",
|
||||
@@ -489,6 +539,75 @@ export const dict = {
|
||||
"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.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.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": "Alerte 01",
|
||||
"sound.option.alert02": "Alerte 02",
|
||||
"sound.option.alert03": "Alerte 03",
|
||||
"sound.option.alert04": "Alerte 04",
|
||||
"sound.option.alert05": "Alerte 05",
|
||||
"sound.option.alert06": "Alerte 06",
|
||||
"sound.option.alert07": "Alerte 07",
|
||||
"sound.option.alert08": "Alerte 08",
|
||||
"sound.option.alert09": "Alerte 09",
|
||||
"sound.option.alert10": "Alerte 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": "Non 01",
|
||||
"sound.option.nope02": "Non 02",
|
||||
"sound.option.nope03": "Non 03",
|
||||
"sound.option.nope04": "Non 04",
|
||||
"sound.option.nope05": "Non 05",
|
||||
"sound.option.nope06": "Non 06",
|
||||
"sound.option.nope07": "Non 07",
|
||||
"sound.option.nope08": "Non 08",
|
||||
"sound.option.nope09": "Non 09",
|
||||
"sound.option.nope10": "Non 10",
|
||||
"sound.option.nope11": "Non 11",
|
||||
"sound.option.nope12": "Non 12",
|
||||
"sound.option.yup01": "Oui 01",
|
||||
"sound.option.yup02": "Oui 02",
|
||||
"sound.option.yup03": "Oui 03",
|
||||
"sound.option.yup04": "Oui 04",
|
||||
"sound.option.yup05": "Oui 05",
|
||||
"sound.option.yup06": "Oui 06",
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Afficher une notification système lorsque l'agent a terminé ou nécessite une attention",
|
||||
@@ -525,6 +644,13 @@ export const dict = {
|
||||
|
||||
"settings.providers.title": "Fournisseurs",
|
||||
"settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.",
|
||||
"settings.providers.section.connected": "Fournisseurs connectés",
|
||||
"settings.providers.connected.empty": "Aucun fournisseur connecté",
|
||||
"settings.providers.section.popular": "Fournisseurs populaires",
|
||||
"settings.providers.tag.environment": "Environnement",
|
||||
"settings.providers.tag.config": "Configuration",
|
||||
"settings.providers.tag.custom": "Personnalisé",
|
||||
"settings.providers.tag.other": "Autre",
|
||||
"settings.models.title": "Modèles",
|
||||
"settings.models.description": "Les paramètres des modèles seront configurables ici.",
|
||||
"settings.agents.title": "Agents",
|
||||
@@ -593,6 +719,7 @@ export const dict = {
|
||||
"workspace.reset.failed.title": "Échec de la réinitialisation de l'espace de travail",
|
||||
"workspace.reset.success.title": "Espace de travail réinitialisé",
|
||||
"workspace.reset.success.description": "L'espace de travail correspond maintenant à la branche par défaut.",
|
||||
"workspace.error.stillPreparing": "L'espace de travail est encore en cours de préparation",
|
||||
"workspace.status.checking": "Vérification des modifications non fusionnées...",
|
||||
"workspace.status.error": "Impossible de vérifier le statut git.",
|
||||
"workspace.status.clean": "Aucune modification non fusionnée détectée.",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
"command.category.theme": "テーマ",
|
||||
"command.category.language": "言語",
|
||||
"command.category.file": "ファイル",
|
||||
"command.category.context": "コンテキスト",
|
||||
"command.category.terminal": "ターミナル",
|
||||
"command.category.model": "モデル",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -15,6 +16,7 @@ export const dict = {
|
||||
"command.category.permissions": "権限",
|
||||
"command.category.workspace": "ワークスペース",
|
||||
|
||||
"command.category.settings": "設定",
|
||||
"theme.scheme.system": "システム",
|
||||
"theme.scheme.light": "ライト",
|
||||
"theme.scheme.dark": "ダーク",
|
||||
@@ -23,6 +25,7 @@ export const dict = {
|
||||
"command.project.open": "プロジェクトを開く",
|
||||
"command.provider.connect": "プロバイダーに接続",
|
||||
"command.server.switch": "サーバーの切り替え",
|
||||
"command.settings.open": "設定を開く",
|
||||
"command.session.previous": "前のセッション",
|
||||
"command.session.next": "次のセッション",
|
||||
"command.session.archive": "セッションをアーカイブ",
|
||||
@@ -40,7 +43,10 @@ export const dict = {
|
||||
"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": "新しいターミナルタブを作成",
|
||||
@@ -116,6 +122,7 @@ export const dict = {
|
||||
"OpenCode Zenは、コーディングエージェント向けに最適化された信頼性の高いモデルへのアクセスを提供します。",
|
||||
"provider.connect.opencodeZen.line2": "1つの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": "このリンク",
|
||||
@@ -133,13 +140,32 @@ export const dict = {
|
||||
"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": "保存中...",
|
||||
@@ -148,6 +174,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "シェルコマンドを入力...",
|
||||
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
||||
"prompt.placeholder.summarizeComment": "コメントを要約…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "escで終了",
|
||||
|
||||
@@ -251,6 +279,10 @@ export const dict = {
|
||||
"dialog.project.edit.color": "色",
|
||||
"dialog.project.edit.color.select": "{{color}}の色を選択",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "ワークスペース起動スクリプト",
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"新しいワークスペース (ワークツリー) を作成した後に実行されます。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例: bun install",
|
||||
"context.breakdown.title": "コンテキストの内訳",
|
||||
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
|
||||
"context.breakdown.system": "システム",
|
||||
@@ -299,6 +331,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "言語",
|
||||
"toast.language.description": "{{language}}に切り替えました",
|
||||
@@ -316,6 +349,9 @@ export const dict = {
|
||||
|
||||
"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をクリップボードにコピーしました!",
|
||||
@@ -390,13 +426,19 @@ export const dict = {
|
||||
"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": "メインブランチ",
|
||||
@@ -438,6 +480,9 @@ export const dict = {
|
||||
"terminal.title.numbered": "ターミナル {{number}}",
|
||||
"terminal.close": "ターミナルを閉じる",
|
||||
|
||||
"terminal.connectionLost.title": "接続が失われました",
|
||||
"terminal.connectionLost.description":
|
||||
"ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。",
|
||||
"common.closeTab": "タブを閉じる",
|
||||
"common.dismiss": "閉じる",
|
||||
"common.requestFailed": "リクエスト失敗",
|
||||
@@ -451,6 +496,8 @@ export const dict = {
|
||||
"common.edit": "編集",
|
||||
"common.loadMore": "さらに読み込む",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "メニューを切り替え",
|
||||
"sidebar.nav.projectsAndSessions": "プロジェクトとセッション",
|
||||
"sidebar.settings": "設定",
|
||||
"sidebar.help": "ヘルプ",
|
||||
@@ -462,12 +509,15 @@ export const dict = {
|
||||
"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": "言語",
|
||||
@@ -479,6 +529,75 @@ export const dict = {
|
||||
"settings.general.row.font.title": "フォント",
|
||||
"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.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": "ビップボップ 01",
|
||||
"sound.option.bipbop02": "ビップボップ 02",
|
||||
"sound.option.bipbop03": "ビップボップ 03",
|
||||
"sound.option.bipbop04": "ビップボップ 04",
|
||||
"sound.option.bipbop05": "ビップボップ 05",
|
||||
"sound.option.bipbop06": "ビップボップ 06",
|
||||
"sound.option.bipbop07": "ビップボップ 07",
|
||||
"sound.option.bipbop08": "ビップボップ 08",
|
||||
"sound.option.bipbop09": "ビップボップ 09",
|
||||
"sound.option.bipbop10": "ビップボップ 10",
|
||||
"sound.option.staplebops01": "ステープルボップス 01",
|
||||
"sound.option.staplebops02": "ステープルボップス 02",
|
||||
"sound.option.staplebops03": "ステープルボップス 03",
|
||||
"sound.option.staplebops04": "ステープルボップス 04",
|
||||
"sound.option.staplebops05": "ステープルボップス 05",
|
||||
"sound.option.staplebops06": "ステープルボップス 06",
|
||||
"sound.option.staplebops07": "ステープルボップス 07",
|
||||
"sound.option.nope01": "いいえ 01",
|
||||
"sound.option.nope02": "いいえ 02",
|
||||
"sound.option.nope03": "いいえ 03",
|
||||
"sound.option.nope04": "いいえ 04",
|
||||
"sound.option.nope05": "いいえ 05",
|
||||
"sound.option.nope06": "いいえ 06",
|
||||
"sound.option.nope07": "いいえ 07",
|
||||
"sound.option.nope08": "いいえ 08",
|
||||
"sound.option.nope09": "いいえ 09",
|
||||
"sound.option.nope10": "いいえ 10",
|
||||
"sound.option.nope11": "いいえ 11",
|
||||
"sound.option.nope12": "いいえ 12",
|
||||
"sound.option.yup01": "はい 01",
|
||||
"sound.option.yup02": "はい 02",
|
||||
"sound.option.yup03": "はい 03",
|
||||
"sound.option.yup04": "はい 04",
|
||||
"sound.option.yup05": "はい 05",
|
||||
"sound.option.yup06": "はい 06",
|
||||
"settings.general.notifications.agent.title": "エージェント",
|
||||
"settings.general.notifications.agent.description":
|
||||
"エージェントが完了したか、注意が必要な場合にシステム通知を表示します",
|
||||
@@ -514,6 +633,13 @@ export const dict = {
|
||||
|
||||
"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": "エージェント",
|
||||
@@ -580,6 +706,7 @@ export const dict = {
|
||||
"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": "未マージの変更は検出されませんでした。",
|
||||
|
||||
@@ -12,6 +12,7 @@ export const dict = {
|
||||
"command.category.theme": "테마",
|
||||
"command.category.language": "언어",
|
||||
"command.category.file": "파일",
|
||||
"command.category.context": "컨텍스트",
|
||||
"command.category.terminal": "터미널",
|
||||
"command.category.model": "모델",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -19,6 +20,7 @@ export const dict = {
|
||||
"command.category.permissions": "권한",
|
||||
"command.category.workspace": "작업 공간",
|
||||
|
||||
"command.category.settings": "설정",
|
||||
"theme.scheme.system": "시스템",
|
||||
"theme.scheme.light": "라이트",
|
||||
"theme.scheme.dark": "다크",
|
||||
@@ -27,6 +29,7 @@ export const dict = {
|
||||
"command.project.open": "프로젝트 열기",
|
||||
"command.provider.connect": "공급자 연결",
|
||||
"command.server.switch": "서버 전환",
|
||||
"command.settings.open": "설정 열기",
|
||||
"command.session.previous": "이전 세션",
|
||||
"command.session.next": "다음 세션",
|
||||
"command.session.archive": "세션 보관",
|
||||
@@ -44,7 +47,10 @@ export const dict = {
|
||||
"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": "새 터미널 탭 생성",
|
||||
@@ -120,6 +126,7 @@ export const dict = {
|
||||
"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": "이 링크",
|
||||
@@ -137,13 +144,32 @@ export const dict = {
|
||||
"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": "저장 중...",
|
||||
@@ -152,6 +178,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "셸 명령어 입력...",
|
||||
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
||||
"prompt.placeholder.summarizeComment": "댓글 요약…",
|
||||
"prompt.mode.shell": "셸",
|
||||
"prompt.mode.shell.exit": "종료하려면 esc",
|
||||
|
||||
@@ -255,6 +283,9 @@ export const dict = {
|
||||
"dialog.project.edit.color": "색상",
|
||||
"dialog.project.edit.color.select": "{{color}} 색상 선택",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트",
|
||||
"dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "예: bun install",
|
||||
"context.breakdown.title": "컨텍스트 분석",
|
||||
"context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.',
|
||||
"context.breakdown.system": "시스템",
|
||||
@@ -303,6 +334,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "언어",
|
||||
"toast.language.description": "{{language}}(으)로 전환됨",
|
||||
@@ -320,6 +352,9 @@ export const dict = {
|
||||
|
||||
"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이 클립보드에 복사되었습니다!",
|
||||
@@ -393,13 +428,19 @@ export const dict = {
|
||||
"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": "메인 브랜치",
|
||||
@@ -440,6 +481,9 @@ export const dict = {
|
||||
"terminal.title.numbered": "터미널 {{number}}",
|
||||
"terminal.close": "터미널 닫기",
|
||||
|
||||
"terminal.connectionLost.title": "연결 끊김",
|
||||
"terminal.connectionLost.description":
|
||||
"터미널 연결이 중단되었습니다. 서버가 재시작하면 이런 일이 발생할 수 있습니다.",
|
||||
"common.closeTab": "탭 닫기",
|
||||
"common.dismiss": "닫기",
|
||||
"common.requestFailed": "요청 실패",
|
||||
@@ -453,6 +497,8 @@ export const dict = {
|
||||
"common.edit": "편집",
|
||||
"common.loadMore": "더 불러오기",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "메뉴 토글",
|
||||
"sidebar.nav.projectsAndSessions": "프로젝트 및 세션",
|
||||
"sidebar.settings": "설정",
|
||||
"sidebar.help": "도움말",
|
||||
@@ -464,12 +510,15 @@ export const dict = {
|
||||
"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": "언어",
|
||||
@@ -481,6 +530,75 @@ export const dict = {
|
||||
"settings.general.row.font.title": "글꼴",
|
||||
"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.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": "빕-밥 01",
|
||||
"sound.option.bipbop02": "빕-밥 02",
|
||||
"sound.option.bipbop03": "빕-밥 03",
|
||||
"sound.option.bipbop04": "빕-밥 04",
|
||||
"sound.option.bipbop05": "빕-밥 05",
|
||||
"sound.option.bipbop06": "빕-밥 06",
|
||||
"sound.option.bipbop07": "빕-밥 07",
|
||||
"sound.option.bipbop08": "빕-밥 08",
|
||||
"sound.option.bipbop09": "빕-밥 09",
|
||||
"sound.option.bipbop10": "빕-밥 10",
|
||||
"sound.option.staplebops01": "스테이플밥스 01",
|
||||
"sound.option.staplebops02": "스테이플밥스 02",
|
||||
"sound.option.staplebops03": "스테이플밥스 03",
|
||||
"sound.option.staplebops04": "스테이플밥스 04",
|
||||
"sound.option.staplebops05": "스테이플밥스 05",
|
||||
"sound.option.staplebops06": "스테이플밥스 06",
|
||||
"sound.option.staplebops07": "스테이플밥스 07",
|
||||
"sound.option.nope01": "아니오 01",
|
||||
"sound.option.nope02": "아니오 02",
|
||||
"sound.option.nope03": "아니오 03",
|
||||
"sound.option.nope04": "아니오 04",
|
||||
"sound.option.nope05": "아니오 05",
|
||||
"sound.option.nope06": "아니오 06",
|
||||
"sound.option.nope07": "아니오 07",
|
||||
"sound.option.nope08": "아니오 08",
|
||||
"sound.option.nope09": "아니오 09",
|
||||
"sound.option.nope10": "아니오 10",
|
||||
"sound.option.nope11": "아니오 11",
|
||||
"sound.option.nope12": "아니오 12",
|
||||
"sound.option.yup01": "네 01",
|
||||
"sound.option.yup02": "네 02",
|
||||
"sound.option.yup03": "네 03",
|
||||
"sound.option.yup04": "네 04",
|
||||
"sound.option.yup05": "네 05",
|
||||
"sound.option.yup06": "네 06",
|
||||
"settings.general.notifications.agent.title": "에이전트",
|
||||
"settings.general.notifications.agent.description": "에이전트가 완료되거나 주의가 필요할 때 시스템 알림 표시",
|
||||
"settings.general.notifications.permissions.title": "권한",
|
||||
@@ -515,6 +633,13 @@ export const dict = {
|
||||
|
||||
"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": "에이전트",
|
||||
@@ -581,6 +706,7 @@ export const dict = {
|
||||
"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": "병합되지 않은 변경 사항이 감지되지 않았습니다.",
|
||||
|
||||
@@ -11,6 +11,7 @@ export const dict = {
|
||||
"command.category.theme": "Tema",
|
||||
"command.category.language": "Språk",
|
||||
"command.category.file": "Fil",
|
||||
"command.category.context": "Kontekst",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modell",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -45,7 +46,10 @@ export const dict = {
|
||||
"command.session.new": "Ny sesjon",
|
||||
"command.file.open": "Åpne fil",
|
||||
"command.file.open.description": "Søk i filer og kommandoer",
|
||||
"command.context.addSelection": "Legg til markering i kontekst",
|
||||
"command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil",
|
||||
"command.terminal.toggle": "Veksle terminal",
|
||||
"command.fileTree.toggle": "Veksle filtre",
|
||||
"command.review.toggle": "Veksle gjennomgang",
|
||||
"command.terminal.new": "Ny terminal",
|
||||
"command.terminal.new.description": "Opprett en ny terminalfane",
|
||||
@@ -140,6 +144,8 @@ export const dict = {
|
||||
"provider.connect.toast.connected.title": "{{provider}} tilkoblet",
|
||||
"provider.connect.toast.connected.description": "{{provider}}-modeller er nå tilgjengelige.",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet",
|
||||
"provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke lenger tilgjengelige.",
|
||||
"model.tag.free": "Gratis",
|
||||
"model.tag.latest": "Nyeste",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
@@ -162,6 +168,8 @@ export const dict = {
|
||||
"common.loading": "Laster",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Avbryt",
|
||||
"common.connect": "Koble til",
|
||||
"common.disconnect": "Koble fra",
|
||||
"common.submit": "Send inn",
|
||||
"common.save": "Lagre",
|
||||
"common.saving": "Lagrer...",
|
||||
@@ -170,6 +178,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "Skriv inn shell-kommando...",
|
||||
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
||||
"prompt.placeholder.summarizeComment": "Oppsummer kommentar…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "ESC for å avslutte",
|
||||
|
||||
@@ -273,6 +283,9 @@ export const dict = {
|
||||
"dialog.project.edit.color": "Farge",
|
||||
"dialog.project.edit.color.select": "Velg fargen {{color}}",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "Oppstartsskript for arbeidsområde",
|
||||
"dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note": 'Omtrentlig fordeling av input-tokens. "Annet" inkluderer verktøydefinisjoner og overhead.',
|
||||
"context.breakdown.system": "System",
|
||||
@@ -321,6 +334,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Språk",
|
||||
"toast.language.description": "Byttet til {{language}}",
|
||||
@@ -338,6 +352,9 @@ export const dict = {
|
||||
|
||||
"toast.file.loadFailed.title": "Kunne ikke laste fil",
|
||||
|
||||
"toast.file.listFailed.title": "Kunne ikke liste filer",
|
||||
"toast.context.noLineSelection.title": "Ingen linjevalg",
|
||||
"toast.context.noLineSelection.description": "Velg først et linjeområde i en filfane.",
|
||||
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til utklippstavlen",
|
||||
"toast.session.share.success.title": "Sesjon delt",
|
||||
"toast.session.share.success.description": "Delings-URL kopiert til utklippstavlen!",
|
||||
@@ -412,8 +429,13 @@ export const dict = {
|
||||
"session.tab.context": "Kontekst",
|
||||
"session.panel.reviewAndFiles": "Gjennomgang og filer",
|
||||
"session.review.filesChanged": "{{count}} filer endret",
|
||||
"session.review.change.one": "Endring",
|
||||
"session.review.change.other": "Endringer",
|
||||
"session.review.loadingChanges": "Laster endringer...",
|
||||
"session.review.empty": "Ingen endringer i denne sesjonen ennå",
|
||||
"session.review.noChanges": "Ingen endringer",
|
||||
"session.files.selectToOpen": "Velg en fil å åpne",
|
||||
"session.files.all": "Alle filer",
|
||||
"session.messages.renderEarlier": "Vis tidligere meldinger",
|
||||
"session.messages.loadingEarlier": "Laster inn tidligere meldinger...",
|
||||
"session.messages.loadEarlier": "Last inn tidligere meldinger",
|
||||
@@ -471,6 +493,7 @@ export const dict = {
|
||||
"common.learnMore": "Lær mer",
|
||||
"common.rename": "Gi nytt navn",
|
||||
"common.reset": "Tilbakestill",
|
||||
"common.archive": "Arkiver",
|
||||
"common.delete": "Slett",
|
||||
"common.close": "Lukk",
|
||||
"common.edit": "Rediger",
|
||||
@@ -489,12 +512,15 @@ export const dict = {
|
||||
"sidebar.project.recentSessions": "Nylige sesjoner",
|
||||
"sidebar.project.viewAllSessions": "Vis alle sesjoner",
|
||||
|
||||
"app.name.desktop": "OpenCode Desktop",
|
||||
"settings.section.desktop": "Skrivebord",
|
||||
"settings.section.server": "Server",
|
||||
"settings.tab.general": "Generelt",
|
||||
"settings.tab.shortcuts": "Snarveier",
|
||||
|
||||
"settings.general.section.appearance": "Utseende",
|
||||
"settings.general.section.notifications": "Systemvarsler",
|
||||
"settings.general.section.updates": "Oppdateringer",
|
||||
"settings.general.section.sounds": "Lydeffekter",
|
||||
|
||||
"settings.general.row.language.title": "Språk",
|
||||
@@ -506,6 +532,75 @@ export const dict = {
|
||||
"settings.general.row.font.title": "Skrift",
|
||||
"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.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": "Varsel 01",
|
||||
"sound.option.alert02": "Varsel 02",
|
||||
"sound.option.alert03": "Varsel 03",
|
||||
"sound.option.alert04": "Varsel 04",
|
||||
"sound.option.alert05": "Varsel 05",
|
||||
"sound.option.alert06": "Varsel 06",
|
||||
"sound.option.alert07": "Varsel 07",
|
||||
"sound.option.alert08": "Varsel 08",
|
||||
"sound.option.alert09": "Varsel 09",
|
||||
"sound.option.alert10": "Varsel 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": "Nei 01",
|
||||
"sound.option.nope02": "Nei 02",
|
||||
"sound.option.nope03": "Nei 03",
|
||||
"sound.option.nope04": "Nei 04",
|
||||
"sound.option.nope05": "Nei 05",
|
||||
"sound.option.nope06": "Nei 06",
|
||||
"sound.option.nope07": "Nei 07",
|
||||
"sound.option.nope08": "Nei 08",
|
||||
"sound.option.nope09": "Nei 09",
|
||||
"sound.option.nope10": "Nei 10",
|
||||
"sound.option.nope11": "Nei 11",
|
||||
"sound.option.nope12": "Nei 12",
|
||||
"sound.option.yup01": "Ja 01",
|
||||
"sound.option.yup02": "Ja 02",
|
||||
"sound.option.yup03": "Ja 03",
|
||||
"sound.option.yup04": "Ja 04",
|
||||
"sound.option.yup05": "Ja 05",
|
||||
"sound.option.yup06": "Ja 06",
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Vis systemvarsel når agenten er ferdig eller trenger oppmerksomhet",
|
||||
@@ -541,6 +636,13 @@ export const dict = {
|
||||
|
||||
"settings.providers.title": "Leverandører",
|
||||
"settings.providers.description": "Leverandørinnstillinger vil kunne konfigureres her.",
|
||||
"settings.providers.section.connected": "Tilkoblede leverandører",
|
||||
"settings.providers.connected.empty": "Ingen tilkoblede leverandører",
|
||||
"settings.providers.section.popular": "Populære leverandører",
|
||||
"settings.providers.tag.environment": "Miljø",
|
||||
"settings.providers.tag.config": "Konfigurasjon",
|
||||
"settings.providers.tag.custom": "Tilpasset",
|
||||
"settings.providers.tag.other": "Annet",
|
||||
"settings.models.title": "Modeller",
|
||||
"settings.models.description": "Modellinnstillinger vil kunne konfigureres her.",
|
||||
"settings.agents.title": "Agenter",
|
||||
@@ -593,6 +695,10 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "Oppdager gjentatte verktøykall med identisk input",
|
||||
|
||||
"session.delete.failed.title": "Kunne ikke slette sesjon",
|
||||
"session.delete.title": "Slett sesjon",
|
||||
"session.delete.confirm": 'Slette sesjonen "{{name}}"?',
|
||||
"session.delete.button": "Slett sesjon",
|
||||
"workspace.new": "Nytt arbeidsområde",
|
||||
"workspace.type.local": "lokal",
|
||||
"workspace.type.sandbox": "sandkasse",
|
||||
@@ -603,6 +709,7 @@ export const dict = {
|
||||
"workspace.reset.failed.title": "Kunne ikke tilbakestille arbeidsområde",
|
||||
"workspace.reset.success.title": "Arbeidsområde tilbakestilt",
|
||||
"workspace.reset.success.description": "Arbeidsområdet samsvarer nå med standardgrenen.",
|
||||
"workspace.error.stillPreparing": "Arbeidsområdet klargjøres fortsatt",
|
||||
"workspace.status.checking": "Sjekker for ikke-sammenslåtte endringer...",
|
||||
"workspace.status.error": "Kunne ikke bekrefte git-status.",
|
||||
"workspace.status.clean": "Ingen ikke-sammenslåtte endringer oppdaget.",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
"command.category.theme": "Motyw",
|
||||
"command.category.language": "Język",
|
||||
"command.category.file": "Plik",
|
||||
"command.category.context": "Kontekst",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Model",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -42,7 +43,10 @@ export const dict = {
|
||||
"command.session.new": "Nowa sesja",
|
||||
"command.file.open": "Otwórz plik",
|
||||
"command.file.open.description": "Szukaj plików i poleceń",
|
||||
"command.context.addSelection": "Dodaj zaznaczenie do kontekstu",
|
||||
"command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku",
|
||||
"command.terminal.toggle": "Przełącz terminal",
|
||||
"command.fileTree.toggle": "Przełącz drzewo plików",
|
||||
"command.review.toggle": "Przełącz przegląd",
|
||||
"command.terminal.new": "Nowy terminal",
|
||||
"command.terminal.new.description": "Utwórz nową kartę terminala",
|
||||
@@ -137,6 +141,8 @@ export const dict = {
|
||||
"provider.connect.toast.connected.title": "Połączono {{provider}}",
|
||||
"provider.connect.toast.connected.description": "Modele {{provider}} są teraz dostępne do użycia.",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "Rozłączono {{provider}}",
|
||||
"provider.disconnect.toast.disconnected.description": "Modele {{provider}} nie są już dostępne.",
|
||||
"model.tag.free": "Darmowy",
|
||||
"model.tag.latest": "Najnowszy",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
@@ -159,6 +165,8 @@ export const dict = {
|
||||
"common.loading": "Ładowanie",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Anuluj",
|
||||
"common.connect": "Połącz",
|
||||
"common.disconnect": "Rozłącz",
|
||||
"common.submit": "Prześlij",
|
||||
"common.save": "Zapisz",
|
||||
"common.saving": "Zapisywanie...",
|
||||
@@ -167,6 +175,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "Wpisz polecenie terminala...",
|
||||
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
||||
"prompt.placeholder.summarizeComment": "Podsumuj komentarz…",
|
||||
"prompt.mode.shell": "Terminal",
|
||||
"prompt.mode.shell.exit": "esc aby wyjść",
|
||||
|
||||
@@ -270,6 +280,10 @@ export const dict = {
|
||||
"dialog.project.edit.color": "Kolor",
|
||||
"dialog.project.edit.color.select": "Wybierz kolor {{color}}",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "Skrypt uruchamiania przestrzeni roboczej",
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Uruchamiany po utworzeniu nowej przestrzeni roboczej (drzewa roboczego).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "np. bun install",
|
||||
"context.breakdown.title": "Podział kontekstu",
|
||||
"context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.',
|
||||
"context.breakdown.system": "System",
|
||||
@@ -318,6 +332,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Język",
|
||||
"toast.language.description": "Przełączono na {{language}}",
|
||||
@@ -335,6 +350,9 @@ export const dict = {
|
||||
|
||||
"toast.file.loadFailed.title": "Nie udało się załadować pliku",
|
||||
|
||||
"toast.file.listFailed.title": "Nie udało się wyświetlić listy plików",
|
||||
"toast.context.noLineSelection.title": "Brak zaznaczenia linii",
|
||||
"toast.context.noLineSelection.description": "Najpierw wybierz zakres linii w zakładce pliku.",
|
||||
"toast.session.share.copyFailed.title": "Nie udało się skopiować URL do schowka",
|
||||
"toast.session.share.success.title": "Sesja udostępniona",
|
||||
"toast.session.share.success.description": "Link udostępniania skopiowany do schowka!",
|
||||
@@ -410,8 +428,13 @@ export const dict = {
|
||||
"session.tab.context": "Kontekst",
|
||||
"session.panel.reviewAndFiles": "Przegląd i pliki",
|
||||
"session.review.filesChanged": "Zmieniono {{count}} plików",
|
||||
"session.review.change.one": "Zmiana",
|
||||
"session.review.change.other": "Zmiany",
|
||||
"session.review.loadingChanges": "Ładowanie zmian...",
|
||||
"session.review.empty": "Brak zmian w tej sesji",
|
||||
"session.review.noChanges": "Brak zmian",
|
||||
"session.files.selectToOpen": "Wybierz plik do otwarcia",
|
||||
"session.files.all": "Wszystkie pliki",
|
||||
"session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości",
|
||||
"session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...",
|
||||
"session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości",
|
||||
@@ -488,12 +511,15 @@ export const dict = {
|
||||
"sidebar.project.recentSessions": "Ostatnie sesje",
|
||||
"sidebar.project.viewAllSessions": "Zobacz wszystkie sesje",
|
||||
|
||||
"app.name.desktop": "OpenCode Desktop",
|
||||
"settings.section.desktop": "Pulpit",
|
||||
"settings.section.server": "Serwer",
|
||||
"settings.tab.general": "Ogólne",
|
||||
"settings.tab.shortcuts": "Skróty",
|
||||
|
||||
"settings.general.section.appearance": "Wygląd",
|
||||
"settings.general.section.notifications": "Powiadomienia systemowe",
|
||||
"settings.general.section.updates": "Aktualizacje",
|
||||
"settings.general.section.sounds": "Efekty dźwiękowe",
|
||||
|
||||
"settings.general.row.language.title": "Język",
|
||||
@@ -504,12 +530,25 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
|
||||
"settings.general.row.font.title": "Czcionka",
|
||||
"settings.general.row.font.description": "Dostosuj czcionkę mono używaną w blokach kodu",
|
||||
|
||||
"settings.general.row.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.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",
|
||||
@@ -597,6 +636,13 @@ export const dict = {
|
||||
|
||||
"settings.providers.title": "Dostawcy",
|
||||
"settings.providers.description": "Ustawienia dostawców będą tutaj konfigurowalne.",
|
||||
"settings.providers.section.connected": "Połączeni dostawcy",
|
||||
"settings.providers.connected.empty": "Brak połączonych dostawców",
|
||||
"settings.providers.section.popular": "Popularni dostawcy",
|
||||
"settings.providers.tag.environment": "Środowisko",
|
||||
"settings.providers.tag.config": "Konfiguracja",
|
||||
"settings.providers.tag.custom": "Niestandardowe",
|
||||
"settings.providers.tag.other": "Inne",
|
||||
"settings.models.title": "Modele",
|
||||
"settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.",
|
||||
"settings.agents.title": "Agenci",
|
||||
@@ -663,6 +709,7 @@ export const dict = {
|
||||
"workspace.reset.failed.title": "Nie udało się zresetować przestrzeni roboczej",
|
||||
"workspace.reset.success.title": "Przestrzeń robocza zresetowana",
|
||||
"workspace.reset.success.description": "Przestrzeń robocza odpowiada teraz domyślnej gałęzi.",
|
||||
"workspace.error.stillPreparing": "Przestrzeń robocza jest wciąż przygotowywana",
|
||||
"workspace.status.checking": "Sprawdzanie niezscalonych zmian...",
|
||||
"workspace.status.error": "Nie można zweryfikować statusu git.",
|
||||
"workspace.status.clean": "Nie wykryto niezscalonych zmian.",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
"command.category.theme": "Тема",
|
||||
"command.category.language": "Язык",
|
||||
"command.category.file": "Файл",
|
||||
"command.category.context": "Контекст",
|
||||
"command.category.terminal": "Терминал",
|
||||
"command.category.model": "Модель",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -42,7 +43,10 @@ export const dict = {
|
||||
"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": "Создать новую вкладку терминала",
|
||||
@@ -137,6 +141,8 @@ export const dict = {
|
||||
"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",
|
||||
@@ -159,6 +165,8 @@ export const dict = {
|
||||
"common.loading": "Загрузка",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Отмена",
|
||||
"common.connect": "Подключить",
|
||||
"common.disconnect": "Отключить",
|
||||
"common.submit": "Отправить",
|
||||
"common.save": "Сохранить",
|
||||
"common.saving": "Сохранение...",
|
||||
@@ -167,6 +175,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "Введите команду оболочки...",
|
||||
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
||||
"prompt.placeholder.summarizeComment": "Суммировать комментарий…",
|
||||
"prompt.mode.shell": "Оболочка",
|
||||
"prompt.mode.shell.exit": "esc для выхода",
|
||||
|
||||
@@ -270,6 +280,10 @@ export const dict = {
|
||||
"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":
|
||||
'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.',
|
||||
@@ -319,6 +333,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Язык",
|
||||
"toast.language.description": "Переключено на {{language}}",
|
||||
@@ -336,6 +351,9 @@ export const dict = {
|
||||
|
||||
"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 скопирован в буфер обмена!",
|
||||
@@ -412,8 +430,13 @@ export const dict = {
|
||||
"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": "Загрузить предыдущие сообщения",
|
||||
@@ -491,12 +514,15 @@ export const dict = {
|
||||
"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": "Язык",
|
||||
@@ -507,12 +533,25 @@ export const dict = {
|
||||
"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": 'Показывать всплывающие окна "Что нового" после обновлений',
|
||||
|
||||
"settings.updates.row.startup.title": "Проверять обновления при запуске",
|
||||
"settings.updates.row.startup.description": "Автоматически проверять обновления при запуске OpenCode",
|
||||
"settings.updates.row.check.title": "Проверить обновления",
|
||||
"settings.updates.row.check.description": "Проверить обновления вручную и установить, если доступны",
|
||||
"settings.updates.action.checkNow": "Проверить сейчас",
|
||||
"settings.updates.action.checking": "Проверка...",
|
||||
"settings.updates.toast.latest.title": "У вас последняя версия",
|
||||
"settings.updates.toast.latest.description": "Вы используете последнюю версию OpenCode.",
|
||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||
"font.option.cascadiaCode": "Cascadia Code",
|
||||
"font.option.firaCode": "Fira Code",
|
||||
"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",
|
||||
@@ -600,6 +639,13 @@ export const dict = {
|
||||
|
||||
"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": "Агенты",
|
||||
@@ -667,6 +713,7 @@ export const dict = {
|
||||
"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": "Неслитые изменения не обнаружены.",
|
||||
|
||||
718
packages/app/src/i18n/th.ts
Normal file
718
packages/app/src/i18n/th.ts
Normal file
@@ -0,0 +1,718 @@
|
||||
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": "สิ่งนี้จะรีเซ็ตพื้นที่ทำงานให้ตรงกับสาขาเริ่มต้น",
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export const dict = {
|
||||
"command.category.theme": "主题",
|
||||
"command.category.language": "语言",
|
||||
"command.category.file": "文件",
|
||||
"command.category.context": "上下文",
|
||||
"command.category.terminal": "终端",
|
||||
"command.category.model": "模型",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -19,6 +20,7 @@ export const dict = {
|
||||
"command.category.permissions": "权限",
|
||||
"command.category.workspace": "工作区",
|
||||
|
||||
"command.category.settings": "设置",
|
||||
"theme.scheme.system": "系统",
|
||||
"theme.scheme.light": "浅色",
|
||||
"theme.scheme.dark": "深色",
|
||||
@@ -27,6 +29,7 @@ export const dict = {
|
||||
"command.project.open": "打开项目",
|
||||
"command.provider.connect": "连接提供商",
|
||||
"command.server.switch": "切换服务器",
|
||||
"command.settings.open": "打开设置",
|
||||
"command.session.previous": "上一个会话",
|
||||
"command.session.next": "下一个会话",
|
||||
"command.session.archive": "归档会话",
|
||||
@@ -34,17 +37,20 @@ export const dict = {
|
||||
"command.palette": "命令面板",
|
||||
|
||||
"command.theme.cycle": "切换主题",
|
||||
"command.theme.set": "使用主题: {{theme}}",
|
||||
"command.theme.set": "使用主题:{{theme}}",
|
||||
"command.theme.scheme.cycle": "切换配色方案",
|
||||
"command.theme.scheme.set": "使用配色方案: {{scheme}}",
|
||||
"command.theme.scheme.set": "使用配色方案:{{scheme}}",
|
||||
|
||||
"command.language.cycle": "切换语言",
|
||||
"command.language.set": "使用语言: {{language}}",
|
||||
"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": "创建新的终端标签页",
|
||||
@@ -92,6 +98,10 @@ export const dict = {
|
||||
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
|
||||
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
|
||||
"dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接",
|
||||
"dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接",
|
||||
"dialog.provider.google.note": "使用 Google 账号或 API 密钥连接",
|
||||
"dialog.provider.openrouter.note": "使用 OpenRouter 账号或 API 密钥连接",
|
||||
"dialog.provider.vercel.note": "使用 Vercel 账号或 API 密钥连接",
|
||||
|
||||
"dialog.model.select.title": "选择模型",
|
||||
"dialog.model.search.placeholder": "搜索模型",
|
||||
@@ -110,7 +120,7 @@ export const dict = {
|
||||
"provider.connect.method.apiKey": "API 密钥",
|
||||
"provider.connect.status.inProgress": "正在授权...",
|
||||
"provider.connect.status.waiting": "等待授权...",
|
||||
"provider.connect.status.failed": "授权失败: {{error}}",
|
||||
"provider.connect.status.failed": "授权失败:{{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。",
|
||||
"provider.connect.apiKey.label": "{{provider}} API 密钥",
|
||||
@@ -119,6 +129,7 @@ export const dict = {
|
||||
"provider.connect.opencodeZen.line1": "OpenCode Zen 为你提供一组精选的可靠优化模型,用于代码智能体。",
|
||||
"provider.connect.opencodeZen.line2": "只需一个 API 密钥,你就能使用 Claude、GPT、Gemini、GLM 等模型。",
|
||||
"provider.connect.opencodeZen.visit.prefix": "访问 ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " 获取你的 API 密钥。",
|
||||
"provider.connect.oauth.code.visit.prefix": "访问 ",
|
||||
"provider.connect.oauth.code.visit.link": "此链接",
|
||||
@@ -134,13 +145,32 @@ export const dict = {
|
||||
"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": "保存中...",
|
||||
@@ -149,34 +179,36 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "输入 shell 命令...",
|
||||
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "总结评论…",
|
||||
"prompt.placeholder.summarizeComment": "总结该评论…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "按 esc 退出",
|
||||
|
||||
"prompt.example.1": "修复代码库中的一个 TODO",
|
||||
"prompt.example.2": "这个项目的技术栈是什么?",
|
||||
"prompt.example.2": "这个项目的技术栈是什么?",
|
||||
"prompt.example.3": "修复失败的测试",
|
||||
"prompt.example.4": "解释认证是如何工作的",
|
||||
"prompt.example.5": "查找并修复安全漏洞",
|
||||
"prompt.example.6": "为用户服务添加单元测试",
|
||||
"prompt.example.7": "重构这个函数,让它更易读",
|
||||
"prompt.example.8": "这个错误是什么意思?",
|
||||
"prompt.example.8": "这个错误是什么意思?",
|
||||
"prompt.example.9": "帮我调试这个问题",
|
||||
"prompt.example.10": "生成 API 文档",
|
||||
"prompt.example.11": "优化数据库查询",
|
||||
"prompt.example.12": "添加输入校验",
|
||||
"prompt.example.13": "创建一个新的组件用于...",
|
||||
"prompt.example.14": "我该如何部署这个项目?",
|
||||
"prompt.example.14": "我该如何部署这个项目?",
|
||||
"prompt.example.15": "审查我的代码并给出最佳实践建议",
|
||||
"prompt.example.16": "为这个函数添加错误处理",
|
||||
"prompt.example.17": "解释这个正则表达式",
|
||||
"prompt.example.18": "把它转换成 TypeScript",
|
||||
"prompt.example.19": "在整个代码库中添加日志",
|
||||
"prompt.example.20": "哪些依赖已经过期?",
|
||||
"prompt.example.20": "哪些依赖已经过期?",
|
||||
"prompt.example.21": "帮我写一个迁移脚本",
|
||||
"prompt.example.22": "为这个接口实现缓存",
|
||||
"prompt.example.23": "给这个列表添加分页",
|
||||
"prompt.example.24": "创建一个 CLI 命令用于...",
|
||||
"prompt.example.25": "这里的环境变量是怎么工作的?",
|
||||
"prompt.example.25": "这里的环境变量是怎么工作的?",
|
||||
|
||||
"prompt.popover.emptyResults": "没有匹配的结果",
|
||||
"prompt.popover.emptyCommands": "没有匹配的命令",
|
||||
@@ -251,6 +283,9 @@ export const dict = {
|
||||
"dialog.project.edit.color": "颜色",
|
||||
"dialog.project.edit.color.select": "选择{{color}}颜色",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "工作区启动脚本",
|
||||
"dialog.project.edit.worktree.startup.description": "在创建新的工作区 (worktree) 后运行。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
|
||||
"context.breakdown.title": "上下文拆分",
|
||||
"context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。",
|
||||
"context.breakdown.system": "系统",
|
||||
@@ -299,6 +334,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "语言",
|
||||
"toast.language.description": "已切换到{{language}}",
|
||||
@@ -316,6 +352,9 @@ export const dict = {
|
||||
|
||||
"toast.file.loadFailed.title": "加载文件失败",
|
||||
|
||||
"toast.file.listFailed.title": "列出文件失败",
|
||||
"toast.context.noLineSelection.title": "未选择行",
|
||||
"toast.context.noLineSelection.description": "请先在文件标签中选择行范围。",
|
||||
"toast.session.share.copyFailed.title": "无法复制链接到剪贴板",
|
||||
"toast.session.share.success.title": "会话已分享",
|
||||
"toast.session.share.success.description": "分享链接已复制到剪贴板",
|
||||
@@ -343,31 +382,31 @@ export const dict = {
|
||||
"error.page.action.updateTo": "更新到 {{version}}",
|
||||
"error.page.report.prefix": "请将此错误报告给 OpenCode 团队",
|
||||
"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.causedBy": "原因:",
|
||||
"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.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) 中的 provider/model 名称",
|
||||
"error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意: OpenCode 暂不支持 MCP 认证。',
|
||||
"error.chain.providerAuthFailed": "提供商认证失败 ({{provider}}): {{message}}",
|
||||
"error.chain.providerAuthFailed": "提供商认证失败({{provider}}):{{message}}",
|
||||
"error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。',
|
||||
"error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)",
|
||||
"error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C): {{message}}",
|
||||
"error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C):{{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。',
|
||||
"error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}",
|
||||
"error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}",
|
||||
"error.chain.configInvalid": "配置文件 {{path}} 无效",
|
||||
"error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效: {{message}}",
|
||||
"error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效:{{message}}",
|
||||
|
||||
"notification.permission.title": "需要权限",
|
||||
"notification.permission.description": "{{sessionTitle}}({{projectName}})需要权限",
|
||||
@@ -388,17 +427,23 @@ export const dict = {
|
||||
"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.mainWithBranch": "主分支({{branch}})",
|
||||
"session.new.worktree.create": "创建新的 worktree",
|
||||
"session.new.lastModified": "最后修改",
|
||||
|
||||
@@ -434,6 +479,8 @@ export const dict = {
|
||||
"terminal.title.numbered": "终端 {{number}}",
|
||||
"terminal.close": "关闭终端",
|
||||
|
||||
"terminal.connectionLost.title": "连接已丢失",
|
||||
"terminal.connectionLost.description": "终端连接已中断。这可能发生在服务器重启时。",
|
||||
"common.closeTab": "关闭标签页",
|
||||
"common.dismiss": "忽略",
|
||||
"common.requestFailed": "请求失败",
|
||||
@@ -447,6 +494,8 @@ export const dict = {
|
||||
"common.edit": "编辑",
|
||||
"common.loadMore": "加载更多",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "切换菜单",
|
||||
"sidebar.nav.projectsAndSessions": "项目和会话",
|
||||
"sidebar.settings": "设置",
|
||||
"sidebar.help": "帮助",
|
||||
@@ -458,12 +507,15 @@ export const dict = {
|
||||
"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": "语言",
|
||||
@@ -474,7 +526,75 @@ export const dict = {
|
||||
"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": "更新后显示“新功能”弹窗",
|
||||
|
||||
"settings.updates.row.startup.title": "启动时检查更新",
|
||||
"settings.updates.row.startup.description": "在 OpenCode 启动时自动检查更新",
|
||||
"settings.updates.row.check.title": "检查更新",
|
||||
"settings.updates.row.check.description": "手动检查更新并在有更新时安装",
|
||||
"settings.updates.action.checkNow": "立即检查",
|
||||
"settings.updates.action.checking": "正在检查...",
|
||||
"settings.updates.toast.latest.title": "已是最新版本",
|
||||
"settings.updates.toast.latest.description": "你正在使用最新版本的 OpenCode。",
|
||||
|
||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||
"font.option.cascadiaCode": "Cascadia Code",
|
||||
"font.option.firaCode": "Fira Code",
|
||||
"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": "哔啵 01",
|
||||
"sound.option.bipbop02": "哔啵 02",
|
||||
"sound.option.bipbop03": "哔啵 03",
|
||||
"sound.option.bipbop04": "哔啵 04",
|
||||
"sound.option.bipbop05": "哔啵 05",
|
||||
"sound.option.bipbop06": "哔啵 06",
|
||||
"sound.option.bipbop07": "哔啵 07",
|
||||
"sound.option.bipbop08": "哔啵 08",
|
||||
"sound.option.bipbop09": "哔啵 09",
|
||||
"sound.option.bipbop10": "哔啵 10",
|
||||
"sound.option.staplebops01": "斯泰普博普斯 01",
|
||||
"sound.option.staplebops02": "斯泰普博普斯 02",
|
||||
"sound.option.staplebops03": "斯泰普博普斯 03",
|
||||
"sound.option.staplebops04": "斯泰普博普斯 04",
|
||||
"sound.option.staplebops05": "斯泰普博普斯 05",
|
||||
"sound.option.staplebops06": "斯泰普博普斯 06",
|
||||
"sound.option.staplebops07": "斯泰普博普斯 07",
|
||||
"sound.option.nope01": "否 01",
|
||||
"sound.option.nope02": "否 02",
|
||||
"sound.option.nope03": "否 03",
|
||||
"sound.option.nope04": "否 04",
|
||||
"sound.option.nope05": "否 05",
|
||||
"sound.option.nope06": "否 06",
|
||||
"sound.option.nope07": "否 07",
|
||||
"sound.option.nope08": "否 08",
|
||||
"sound.option.nope09": "否 09",
|
||||
"sound.option.nope10": "否 10",
|
||||
"sound.option.nope11": "否 11",
|
||||
"sound.option.nope12": "否 12",
|
||||
"sound.option.yup01": "是 01",
|
||||
"sound.option.yup02": "是 02",
|
||||
"sound.option.yup03": "是 03",
|
||||
"sound.option.yup04": "是 04",
|
||||
"sound.option.yup05": "是 05",
|
||||
"sound.option.yup06": "是 06",
|
||||
"settings.general.notifications.agent.title": "智能体",
|
||||
"settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知",
|
||||
"settings.general.notifications.permissions.title": "权限",
|
||||
@@ -509,6 +629,13 @@ export const dict = {
|
||||
|
||||
"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": "智能体",
|
||||
@@ -562,7 +689,7 @@ export const dict = {
|
||||
|
||||
"session.delete.failed.title": "删除会话失败",
|
||||
"session.delete.title": "删除会话",
|
||||
"session.delete.confirm": '删除会话 "{{name}}"?',
|
||||
"session.delete.confirm": '删除会话 "{{name}}"?',
|
||||
"session.delete.button": "删除会话",
|
||||
|
||||
"workspace.new": "新建工作区",
|
||||
@@ -575,15 +702,16 @@ export const dict = {
|
||||
"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.confirm": '删除工作区 "{{name}}"?',
|
||||
"workspace.delete.button": "删除工作区",
|
||||
"workspace.reset.title": "重置工作区",
|
||||
"workspace.reset.confirm": '重置工作区 "{{name}}"?',
|
||||
"workspace.reset.confirm": '重置工作区 "{{name}}"?',
|
||||
"workspace.reset.button": "重置工作区",
|
||||
"workspace.reset.archived.none": "不会归档任何活跃会话。",
|
||||
"workspace.reset.archived.one": "将归档 1 个会话。",
|
||||
|
||||
@@ -12,6 +12,7 @@ export const dict = {
|
||||
"command.category.theme": "主題",
|
||||
"command.category.language": "語言",
|
||||
"command.category.file": "檔案",
|
||||
"command.category.context": "上下文",
|
||||
"command.category.terminal": "終端機",
|
||||
"command.category.model": "模型",
|
||||
"command.category.mcp": "MCP",
|
||||
@@ -19,6 +20,7 @@ export const dict = {
|
||||
"command.category.permissions": "權限",
|
||||
"command.category.workspace": "工作區",
|
||||
|
||||
"command.category.settings": "設定",
|
||||
"theme.scheme.system": "系統",
|
||||
"theme.scheme.light": "淺色",
|
||||
"theme.scheme.dark": "深色",
|
||||
@@ -27,6 +29,7 @@ export const dict = {
|
||||
"command.project.open": "開啟專案",
|
||||
"command.provider.connect": "連接提供者",
|
||||
"command.server.switch": "切換伺服器",
|
||||
"command.settings.open": "開啟設定",
|
||||
"command.session.previous": "上一個工作階段",
|
||||
"command.session.next": "下一個工作階段",
|
||||
"command.session.archive": "封存工作階段",
|
||||
@@ -44,7 +47,10 @@ export const dict = {
|
||||
"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": "建立新的終端機標籤頁",
|
||||
@@ -136,13 +142,32 @@ export const dict = {
|
||||
"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": "儲存中...",
|
||||
@@ -151,6 +176,8 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.shell": "輸入 shell 命令...",
|
||||
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "摘要評論…",
|
||||
"prompt.placeholder.summarizeComment": "摘要這則評論…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "按 esc 退出",
|
||||
|
||||
@@ -253,6 +280,9 @@ export const dict = {
|
||||
"dialog.project.edit.color": "顏色",
|
||||
"dialog.project.edit.color.select": "選擇{{color}}顏色",
|
||||
|
||||
"dialog.project.edit.worktree.startup": "工作區啟動腳本",
|
||||
"dialog.project.edit.worktree.startup.description": "在建立新的工作區 (worktree) 後執行。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
|
||||
"context.breakdown.title": "上下文拆分",
|
||||
"context.breakdown.note": "輸入 token 的大致拆分。「其他」包含工具定義和額外開銷。",
|
||||
"context.breakdown.system": "系統",
|
||||
@@ -301,6 +331,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "語言",
|
||||
"toast.language.description": "已切換到 {{language}}",
|
||||
@@ -318,6 +349,9 @@ export const dict = {
|
||||
|
||||
"toast.file.loadFailed.title": "載入檔案失敗",
|
||||
|
||||
"toast.file.listFailed.title": "列出檔案失敗",
|
||||
"toast.context.noLineSelection.title": "未選取行",
|
||||
"toast.context.noLineSelection.description": "請先在檔案分頁中選取行範圍。",
|
||||
"toast.session.share.copyFailed.title": "無法複製連結到剪貼簿",
|
||||
"toast.session.share.success.title": "工作階段已分享",
|
||||
"toast.session.share.success.description": "分享連結已複製到剪貼簿",
|
||||
@@ -390,13 +424,19 @@ export const dict = {
|
||||
"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": "主分支",
|
||||
@@ -436,6 +476,8 @@ export const dict = {
|
||||
"terminal.title.numbered": "終端機 {{number}}",
|
||||
"terminal.close": "關閉終端機",
|
||||
|
||||
"terminal.connectionLost.title": "連線中斷",
|
||||
"terminal.connectionLost.description": "終端機連線已中斷。這可能會在伺服器重新啟動時發生。",
|
||||
"common.closeTab": "關閉標籤頁",
|
||||
"common.dismiss": "忽略",
|
||||
"common.requestFailed": "要求失敗",
|
||||
@@ -449,6 +491,8 @@ export const dict = {
|
||||
"common.edit": "編輯",
|
||||
"common.loadMore": "載入更多",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "切換選單",
|
||||
"sidebar.nav.projectsAndSessions": "專案與工作階段",
|
||||
"sidebar.settings": "設定",
|
||||
"sidebar.help": "說明",
|
||||
@@ -460,12 +504,15 @@ export const dict = {
|
||||
"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": "語言",
|
||||
@@ -477,6 +524,75 @@ export const dict = {
|
||||
"settings.general.row.font.title": "字型",
|
||||
"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.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": "嗶啵 01",
|
||||
"sound.option.bipbop02": "嗶啵 02",
|
||||
"sound.option.bipbop03": "嗶啵 03",
|
||||
"sound.option.bipbop04": "嗶啵 04",
|
||||
"sound.option.bipbop05": "嗶啵 05",
|
||||
"sound.option.bipbop06": "嗶啵 06",
|
||||
"sound.option.bipbop07": "嗶啵 07",
|
||||
"sound.option.bipbop08": "嗶啵 08",
|
||||
"sound.option.bipbop09": "嗶啵 09",
|
||||
"sound.option.bipbop10": "嗶啵 10",
|
||||
"sound.option.staplebops01": "斯泰普博普斯 01",
|
||||
"sound.option.staplebops02": "斯泰普博普斯 02",
|
||||
"sound.option.staplebops03": "斯泰普博普斯 03",
|
||||
"sound.option.staplebops04": "斯泰普博普斯 04",
|
||||
"sound.option.staplebops05": "斯泰普博普斯 05",
|
||||
"sound.option.staplebops06": "斯泰普博普斯 06",
|
||||
"sound.option.staplebops07": "斯泰普博普斯 07",
|
||||
"sound.option.nope01": "否 01",
|
||||
"sound.option.nope02": "否 02",
|
||||
"sound.option.nope03": "否 03",
|
||||
"sound.option.nope04": "否 04",
|
||||
"sound.option.nope05": "否 05",
|
||||
"sound.option.nope06": "否 06",
|
||||
"sound.option.nope07": "否 07",
|
||||
"sound.option.nope08": "否 08",
|
||||
"sound.option.nope09": "否 09",
|
||||
"sound.option.nope10": "否 10",
|
||||
"sound.option.nope11": "否 11",
|
||||
"sound.option.nope12": "否 12",
|
||||
"sound.option.yup01": "是 01",
|
||||
"sound.option.yup02": "是 02",
|
||||
"sound.option.yup03": "是 03",
|
||||
"sound.option.yup04": "是 04",
|
||||
"sound.option.yup05": "是 05",
|
||||
"sound.option.yup06": "是 06",
|
||||
"settings.general.notifications.agent.title": "代理程式",
|
||||
"settings.general.notifications.agent.description": "當代理程式完成或需要注意時顯示系統通知",
|
||||
"settings.general.notifications.permissions.title": "權限",
|
||||
@@ -511,6 +627,13 @@ export const dict = {
|
||||
|
||||
"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": "代理程式",
|
||||
@@ -577,6 +700,7 @@ export const dict = {
|
||||
"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": "未偵測到未合併的變更。",
|
||||
|
||||
@@ -1,84 +1 @@
|
||||
@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,53 +0,0 @@
|
||||
import { CURRENT_RELEASE } from "@/components/dialog-release-notes"
|
||||
|
||||
const STORAGE_KEY = "opencode:last-seen-version"
|
||||
|
||||
// ============================================================================
|
||||
// DEV MODE: Set this to true to always show the release notes modal on startup
|
||||
// Set to false for production behavior (only shows after updates)
|
||||
// ============================================================================
|
||||
const DEV_ALWAYS_SHOW_RELEASE_NOTES = true
|
||||
|
||||
/**
|
||||
* Check if release notes should be shown
|
||||
* Returns true if:
|
||||
* - DEV_ALWAYS_SHOW_RELEASE_NOTES is true (for development)
|
||||
* - OR the current version is newer than the last seen version
|
||||
*/
|
||||
export function shouldShowReleaseNotes(): boolean {
|
||||
if (DEV_ALWAYS_SHOW_RELEASE_NOTES) {
|
||||
console.log("[ReleaseNotes] DEV mode: always showing release notes")
|
||||
return true
|
||||
}
|
||||
|
||||
const lastSeen = localStorage.getItem(STORAGE_KEY)
|
||||
if (!lastSeen) {
|
||||
// First time user - show release notes
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare versions - show if current is newer
|
||||
return CURRENT_RELEASE.version !== lastSeen
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the current release notes as seen
|
||||
* Call this when the user closes the release notes modal
|
||||
*/
|
||||
export function markReleaseNotesSeen(): void {
|
||||
localStorage.setItem(STORAGE_KEY, CURRENT_RELEASE.version)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current version
|
||||
*/
|
||||
export function getCurrentVersion(): string {
|
||||
return CURRENT_RELEASE.version
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the seen status (useful for testing)
|
||||
*/
|
||||
export function resetReleaseNotesSeen(): void {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
@@ -1,22 +1,36 @@
|
||||
import { createMemo, Show, type ParentProps } from "solid-js"
|
||||
import { createEffect, createMemo, Show, type ParentProps } from "solid-js"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||
import { SyncProvider, useSync } from "@/context/sync"
|
||||
import { LocalProvider } from "@/context/local"
|
||||
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const language = useLanguage()
|
||||
const directory = createMemo(() => {
|
||||
return base64Decode(params.dir!)
|
||||
return decode64(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 (
|
||||
<Show when={params.dir}>
|
||||
<Show when={directory()}>
|
||||
<SDKProvider directory={directory()}>
|
||||
<SyncProvider>
|
||||
{iife(() => {
|
||||
|
||||
@@ -23,6 +23,11 @@ export default function Home() {
|
||||
const server = useServer()
|
||||
const language = useLanguage()
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
const recent = createMemo(() => {
|
||||
return sync.data.project
|
||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice(0, 5)
|
||||
})
|
||||
|
||||
function openProject(directory: string) {
|
||||
layout.projects.open(directory)
|
||||
@@ -84,11 +89,7 @@ export default function Home() {
|
||||
</Button>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<For
|
||||
each={sync.data.project
|
||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice(0, 5)}
|
||||
>
|
||||
<For each={recent()}>
|
||||
{(project) => (
|
||||
<Button
|
||||
size="large"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user