Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8257a20aa5 | ||
|
|
c2f78224ae | ||
|
|
14f9e21d5c | ||
|
|
8e4bab5181 | ||
|
|
3c32013eb1 | ||
|
|
47d2ab120a | ||
|
|
186af2723d | ||
|
|
6926fe1c74 | ||
|
|
ee018d5c82 | ||
|
|
0465579d6b | ||
|
|
196a03caff | ||
|
|
b234370080 | ||
|
|
5d2dc8888c | ||
|
|
0b1018f6dd | ||
|
|
afb6abff73 | ||
|
|
e7f94f9b9a | ||
|
|
72c77d0e7b | ||
|
|
5c15755a10 | ||
|
|
3a4bfeb5b5 | ||
|
|
1037c72d99 | ||
|
|
ba00e9a993 | ||
|
|
963dad75ef | ||
|
|
7e9b721e97 | ||
|
|
a5b1dc081d | ||
|
|
0bc2f99f2d | ||
|
|
55895d0663 | ||
|
|
72cb9dfa31 | ||
|
|
f0a9075fdf | ||
|
|
fee1e25ab4 | ||
|
|
a94ac5aa2c | ||
|
|
62ac45a9c9 | ||
|
|
f7c2ef876f | ||
|
|
6639f92739 | ||
|
|
36aeb32159 | ||
|
|
ff37d7c2df | ||
|
|
4f96eb239f | ||
|
|
38af99dcb4 | ||
|
|
772059acb5 | ||
|
|
1f290fc1ba | ||
|
|
77d4f99497 | ||
|
|
aa2d753e7e | ||
|
|
860531c275 | ||
|
|
2b86b36c8c | ||
|
|
8ac2fbbd12 | ||
|
|
26382c6216 | ||
|
|
0981b8eb71 | ||
|
|
aa9ed001d3 | ||
|
|
6086072567 | ||
|
|
6c14ea1d22 | ||
|
|
c3a9ec4a99 | ||
|
|
41b0d03f6a | ||
|
|
81eb6e670b | ||
|
|
8446719b13 | ||
|
|
15a8c22a26 | ||
|
|
48326e8d9c | ||
|
|
43bc5551e8 | ||
|
|
f736116967 | ||
|
|
82fc493520 | ||
|
|
2145d97f18 | ||
|
|
f3997d8082 | ||
|
|
02b19bc3d7 | ||
|
|
5cd54ec345 | ||
|
|
c8909908f5 |
@@ -9,7 +9,8 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync-locales:
|
sync-locales:
|
||||||
if: github.actor != 'opencode-agent[bot]'
|
if: false
|
||||||
|
#if: github.actor != 'opencode-agent[bot]'
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -34,7 +35,7 @@ jobs:
|
|||||||
- name: Compute changed English docs
|
- name: Compute changed English docs
|
||||||
id: changes
|
id: changes
|
||||||
run: |
|
run: |
|
||||||
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'packages/web/src/content/docs/*.mdx' || true)
|
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- ':(glob)packages/web/src/content/docs/*.mdx' || true)
|
||||||
if [ -z "$FILES" ]; then
|
if [ -z "$FILES" ]; then
|
||||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "No English docs changed in push range"
|
echo "No English docs changed in push range"
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ on:
|
|||||||
- "patches/**"
|
- "patches/**"
|
||||||
- ".github/workflows/nix-hashes.yml"
|
- ".github/workflows/nix-hashes.yml"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Native runners required: bun install cross-compilation flags (--os/--cpu)
|
# Native runners required: bun install cross-compilation flags (--os/--cpu)
|
||||||
# do not produce byte-identical node_modules as native installs.
|
# do not produce byte-identical node_modules as native installs.
|
||||||
|
|||||||
@@ -98,15 +98,129 @@ jobs:
|
|||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
path: packages/opencode/dist
|
path: |
|
||||||
|
packages/opencode/dist/opencode-darwin*
|
||||||
|
packages/opencode/dist/opencode-linux*
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-windows
|
||||||
|
path: packages/opencode/dist/opencode-windows*
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ needs.version.outputs.version }}
|
version: ${{ needs.version.outputs.version }}
|
||||||
|
|
||||||
|
sign-cli-windows:
|
||||||
|
needs:
|
||||||
|
- build-cli
|
||||||
|
- version
|
||||||
|
runs-on: blacksmith-4vcpu-windows-2025
|
||||||
|
if: github.repository == 'anomalyco/opencode'
|
||||||
|
env:
|
||||||
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||||
|
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-windows
|
||||||
|
path: packages/opencode/dist
|
||||||
|
|
||||||
|
- name: Setup git committer
|
||||||
|
id: committer
|
||||||
|
uses: ./.github/actions/setup-git-committer
|
||||||
|
with:
|
||||||
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
|
- name: Azure login
|
||||||
|
uses: azure/login@v2
|
||||||
|
with:
|
||||||
|
client-id: ${{ env.AZURE_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
|
- uses: azure/artifact-signing-action@v1
|
||||||
|
with:
|
||||||
|
endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
|
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||||
|
certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||||
|
files: |
|
||||||
|
${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe
|
||||||
|
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
|
||||||
|
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe
|
||||||
|
exclude-environment-credential: true
|
||||||
|
exclude-workload-identity-credential: true
|
||||||
|
exclude-managed-identity-credential: true
|
||||||
|
exclude-shared-token-cache-credential: true
|
||||||
|
exclude-visual-studio-credential: true
|
||||||
|
exclude-visual-studio-code-credential: true
|
||||||
|
exclude-azure-cli-credential: false
|
||||||
|
exclude-azure-powershell-credential: true
|
||||||
|
exclude-azure-developer-cli-credential: true
|
||||||
|
exclude-interactive-browser-credential: true
|
||||||
|
|
||||||
|
- name: Verify Windows CLI signatures
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$files = @(
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe",
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe",
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$sig = Get-AuthenticodeSignature $file
|
||||||
|
if ($sig.Status -ne "Valid") {
|
||||||
|
throw "Invalid signature for ${file}: $($sig.Status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Repack Windows CLI archives
|
||||||
|
working-directory: packages/opencode/dist
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force
|
||||||
|
Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force
|
||||||
|
Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force
|
||||||
|
|
||||||
|
- name: Upload signed Windows CLI release assets
|
||||||
|
if: needs.version.outputs.release != ''
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
run: |
|
||||||
|
gh release upload "v${{ needs.version.outputs.version }}" `
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" `
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" `
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" `
|
||||||
|
--clobber `
|
||||||
|
--repo "${{ needs.version.outputs.repo }}"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-signed-windows
|
||||||
|
path: |
|
||||||
|
packages/opencode/dist/opencode-windows-arm64
|
||||||
|
packages/opencode/dist/opencode-windows-x64
|
||||||
|
packages/opencode/dist/opencode-windows-x64-baseline
|
||||||
|
|
||||||
build-tauri:
|
build-tauri:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
|
env:
|
||||||
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||||
|
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -152,6 +266,14 @@ jobs:
|
|||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Azure login
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: azure/login@v2
|
||||||
|
with:
|
||||||
|
client-id: ${{ env.AZURE_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
@@ -190,6 +312,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
|
||||||
RUST_TARGET: ${{ matrix.settings.target }}
|
RUST_TARGET: ${{ matrix.settings.target }}
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
@@ -246,11 +369,34 @@ jobs:
|
|||||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||||
|
|
||||||
|
- name: Verify signed Windows desktop artifacts
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$files = @(
|
||||||
|
"${{ github.workspace }}\packages\desktop\src-tauri\sidecars\opencode-cli-${{ matrix.settings.target }}.exe"
|
||||||
|
)
|
||||||
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\src-tauri\target\${{ matrix.settings.target }}\release\bundle\nsis\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
|
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$sig = Get-AuthenticodeSignature $file
|
||||||
|
if ($sig.Status -ne "Valid") {
|
||||||
|
throw "Invalid signature for ${file}: $($sig.Status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
build-electron:
|
build-electron:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
|
env:
|
||||||
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||||
|
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -292,6 +438,14 @@ jobs:
|
|||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Azure login
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: azure/login@v2
|
||||||
|
with:
|
||||||
|
client-id: ${{ env.AZURE_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
@@ -326,6 +480,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
|
||||||
RUST_TARGET: ${{ matrix.settings.target }}
|
RUST_TARGET: ${{ matrix.settings.target }}
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
@@ -358,6 +513,22 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
|
||||||
|
- name: Verify signed Windows Electron artifacts
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$files = @()
|
||||||
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
|
||||||
|
|
||||||
|
foreach ($file in $files | Select-Object -Unique) {
|
||||||
|
$sig = Get-AuthenticodeSignature $file
|
||||||
|
if ($sig.Status -ne "Valid") {
|
||||||
|
throw "Invalid signature for ${file}: $($sig.Status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-electron-${{ matrix.settings.target }}
|
name: opencode-electron-${{ matrix.settings.target }}
|
||||||
@@ -373,6 +544,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- version
|
- version
|
||||||
- build-cli
|
- build-cli
|
||||||
|
- sign-cli-windows
|
||||||
- build-tauri
|
- build-tauri
|
||||||
- build-electron
|
- build-electron
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
@@ -411,6 +583,16 @@ jobs:
|
|||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-windows
|
||||||
|
path: packages/opencode/dist
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-signed-windows
|
||||||
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
name: sign-cli
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- brendan/desktop-signpath
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sign-cli:
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
if: github.repository == 'anomalyco/opencode'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-tags: true
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
./packages/opencode/script/build.ts
|
|
||||||
|
|
||||||
- name: Upload unsigned Windows CLI
|
|
||||||
id: upload_unsigned_windows_cli
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: unsigned-opencode-windows-cli
|
|
||||||
path: packages/opencode/dist/opencode-windows-x64/bin/opencode.exe
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Submit SignPath signing request
|
|
||||||
id: submit_signpath_signing_request
|
|
||||||
uses: signpath/github-action-submit-signing-request@v1
|
|
||||||
with:
|
|
||||||
api-token: ${{ secrets.SIGNPATH_API_KEY }}
|
|
||||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
|
||||||
project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
|
|
||||||
signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
|
|
||||||
artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
|
|
||||||
github-artifact-id: ${{ steps.upload_unsigned_windows_cli.outputs.artifact-id }}
|
|
||||||
wait-for-completion: true
|
|
||||||
output-artifact-directory: signed-opencode-cli
|
|
||||||
|
|
||||||
- name: Upload signed Windows CLI
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: signed-opencode-windows-cli
|
|
||||||
path: signed-opencode-cli/*.exe
|
|
||||||
if-no-files-found: error
|
|
||||||
@@ -25,6 +25,7 @@ target
|
|||||||
|
|
||||||
# Local dev files
|
# Local dev files
|
||||||
opencode-dev
|
opencode-dev
|
||||||
|
UPCOMING_CHANGELOG.md
|
||||||
logs/
|
logs/
|
||||||
*.bun-build
|
*.bun-build
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
description: ALWAYS use this when writing docs
|
|
||||||
color: "#38A3EE"
|
|
||||||
---
|
|
||||||
|
|
||||||
You are an expert technical documentation writer
|
|
||||||
|
|
||||||
You are not verbose
|
|
||||||
|
|
||||||
Use a relaxed and friendly tone
|
|
||||||
|
|
||||||
The title of the page should be a word or a 2-3 word phrase
|
|
||||||
|
|
||||||
The description should be one short line, should not start with "The", should
|
|
||||||
avoid repeating the title of the page, should be 5-10 words long
|
|
||||||
|
|
||||||
Chunks of text should not be more than 2 sentences long
|
|
||||||
|
|
||||||
Each section is separated by a divider of 3 dashes
|
|
||||||
|
|
||||||
The section titles are short with only the first letter of the word capitalized
|
|
||||||
|
|
||||||
The section titles are in the imperative mood
|
|
||||||
|
|
||||||
The section titles should not repeat the term used in the page title, for
|
|
||||||
example, if the page title is "Models", avoid using a section title like "Add
|
|
||||||
new models". This might be unavoidable in some cases, but try to avoid it.
|
|
||||||
|
|
||||||
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
|
|
||||||
|
|
||||||
For JS or TS code snippets remove trailing semicolons and any trailing commas
|
|
||||||
that might not be needed.
|
|
||||||
|
|
||||||
If you are making a commit prefix the commit message with `docs:`
|
|
||||||
@@ -2,22 +2,43 @@
|
|||||||
model: opencode/kimi-k2.5
|
model: opencode/kimi-k2.5
|
||||||
---
|
---
|
||||||
|
|
||||||
create UPCOMING_CHANGELOG.md
|
Create `UPCOMING_CHANGELOG.md` from the structured changelog input below.
|
||||||
|
If `UPCOMING_CHANGELOG.md` already exists, ignore its current contents completely.
|
||||||
|
Do not preserve, merge, or reuse text from the existing file.
|
||||||
|
|
||||||
it should have sections
|
Any command arguments are passed directly to `bun script/changelog.ts`.
|
||||||
|
Use `--from` / `-f` and `--to` / `-t` to preview a specific release range.
|
||||||
|
|
||||||
```
|
The input already contains the exact commit range since the last non-draft release.
|
||||||
## TUI
|
The commits are already filtered to the release-relevant packages and grouped into
|
||||||
|
the release sections. Do not fetch GitHub releases, PRs, or build your own commit list.
|
||||||
|
The input may also include a `## Community Contributors Input` section.
|
||||||
|
|
||||||
## Desktop
|
Before writing any entry you keep, inspect the real diff with
|
||||||
|
`git show --stat --format='' <hash>` or `git show --format='' <hash>` so the
|
||||||
|
summary reflects the actual user-facing change and not just the commit message.
|
||||||
|
Do not use `git log` or author metadata when deciding attribution.
|
||||||
|
|
||||||
## Core
|
Rules:
|
||||||
|
|
||||||
## Misc
|
- Write the final file with sections in this order:
|
||||||
```
|
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
|
||||||
|
- Only include sections that have at least one notable entry
|
||||||
|
- Keep one bullet per commit you keep
|
||||||
|
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
|
||||||
|
- Start each bullet with a capital letter
|
||||||
|
- Prefer what changed for users over what code changed internally
|
||||||
|
- Do not copy raw commit prefixes like `fix:` or `feat:` or trailing PR numbers like `(#123)`
|
||||||
|
- Community attribution is deterministic: only preserve an existing `(@username)` suffix from the changelog input
|
||||||
|
- If an input bullet has no `(@username)` suffix, do not add one
|
||||||
|
- Never add a new `(@username)` suffix from `git show`, commit authors, names, or email addresses
|
||||||
|
- If no notable entries remain and there is no contributor block, write exactly `No notable changes.`
|
||||||
|
- If no notable entries remain but there is a contributor block, omit all release sections and return only the contributor block
|
||||||
|
- If the input contains `## Community Contributors Input`, append the block below that heading to the end of the final file verbatim
|
||||||
|
- Do not add, remove, rewrite, or reorder contributor names or commit titles in that block
|
||||||
|
- Do not derive the thank-you section from the main summary bullets
|
||||||
|
- Do not include the heading `## Community Contributors Input` in the final file
|
||||||
|
|
||||||
fetch the latest github release for this repository to determine the last release version.
|
## Changelog Input
|
||||||
|
|
||||||
find each PR that was merged since the last release
|
!`bun script/changelog.ts $ARGUMENTS`
|
||||||
|
|
||||||
for each PR spawn a subagent to summarize what the PR was about. focus on user facing changes. if it was entirely internal or code related you can ignore it. also skip docs updates. each subagent should append its summary to UPCOMING_CHANGELOG.md into the appropriate section.
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||||
import { RGBA, VignetteEffect } from "@opentui/core"
|
import { RGBA, VignetteEffect } from "@opentui/core"
|
||||||
import type { TuiKeybindSet, TuiPluginApi, TuiPluginMeta, TuiSlotPlugin } from "@opencode-ai/plugin/tui"
|
import type {
|
||||||
|
TuiKeybindSet,
|
||||||
|
TuiPlugin,
|
||||||
|
TuiPluginApi,
|
||||||
|
TuiPluginMeta,
|
||||||
|
TuiPluginModule,
|
||||||
|
TuiSlotPlugin,
|
||||||
|
} from "@opencode-ai/plugin/tui"
|
||||||
|
|
||||||
const tabs = ["overview", "counter", "help"]
|
const tabs = ["overview", "counter", "help"]
|
||||||
const bind = {
|
const bind = {
|
||||||
@@ -608,7 +615,7 @@ const Modal = (props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const home = (input: Cfg): TuiSlotPlugin => ({
|
const home = (api: TuiPluginApi, input: Cfg) => ({
|
||||||
slots: {
|
slots: {
|
||||||
home_logo(ctx) {
|
home_logo(ctx) {
|
||||||
const map = ctx.theme.current
|
const map = ctx.theme.current
|
||||||
@@ -642,6 +649,36 @@ const home = (input: Cfg): TuiSlotPlugin => ({
|
|||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
home_prompt(ctx, value) {
|
||||||
|
const skin = look(ctx.theme.current)
|
||||||
|
type Prompt = (props: {
|
||||||
|
workspaceID?: string
|
||||||
|
hint?: JSX.Element
|
||||||
|
placeholders?: {
|
||||||
|
normal?: string[]
|
||||||
|
shell?: string[]
|
||||||
|
}
|
||||||
|
}) => JSX.Element
|
||||||
|
if (!("Prompt" in api.ui)) return null
|
||||||
|
const view = api.ui.Prompt
|
||||||
|
if (typeof view !== "function") return null
|
||||||
|
const Prompt = view as Prompt
|
||||||
|
const normal = [
|
||||||
|
`[SMOKE] route check for ${input.label}`,
|
||||||
|
"[SMOKE] confirm home_prompt slot override",
|
||||||
|
"[SMOKE] verify api.ui.Prompt rendering",
|
||||||
|
]
|
||||||
|
const shell = ["printf '[SMOKE] home prompt\n'", "git status --short", "bun --version"]
|
||||||
|
const Hint = (
|
||||||
|
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
<span style={{ fg: skin.accent }}>•</span> smoke home prompt
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
|
||||||
|
return <Prompt workspaceID={value.workspace_id} hint={Hint} placeholders={{ normal, shell }} />
|
||||||
|
},
|
||||||
home_bottom(ctx) {
|
home_bottom(ctx) {
|
||||||
const skin = look(ctx.theme.current)
|
const skin = look(ctx.theme.current)
|
||||||
const text = "extra content in the unified home bottom slot"
|
const text = "extra content in the unified home bottom slot"
|
||||||
@@ -699,8 +736,8 @@ const block = (input: Cfg, order: number, title: string, text: string): TuiSlotP
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const slot = (input: Cfg): TuiSlotPlugin[] => [
|
const slot = (api: TuiPluginApi, input: Cfg): TuiSlotPlugin[] => [
|
||||||
home(input),
|
home(api, input),
|
||||||
block(input, 50, "Smoke above", "renders above internal sidebar blocks"),
|
block(input, 50, "Smoke above", "renders above internal sidebar blocks"),
|
||||||
block(input, 250, "Smoke between", "renders between internal sidebar blocks"),
|
block(input, 250, "Smoke between", "renders between internal sidebar blocks"),
|
||||||
block(input, 650, "Smoke below", "renders below internal sidebar blocks"),
|
block(input, 650, "Smoke below", "renders below internal sidebar blocks"),
|
||||||
@@ -813,7 +850,7 @@ const reg = (api: TuiPluginApi, input: Cfg, keys: Keys) => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const tui = async (api: TuiPluginApi, options: Record<string, unknown> | null, meta: TuiPluginMeta) => {
|
const tui: TuiPlugin = async (api, options, meta) => {
|
||||||
if (options?.enabled === false) return
|
if (options?.enabled === false) return
|
||||||
|
|
||||||
await api.theme.install("./smoke-theme.json")
|
await api.theme.install("./smoke-theme.json")
|
||||||
@@ -841,12 +878,14 @@ const tui = async (api: TuiPluginApi, options: Record<string, unknown> | null, m
|
|||||||
])
|
])
|
||||||
|
|
||||||
reg(api, value, keys)
|
reg(api, value, keys)
|
||||||
for (const item of slot(value)) {
|
for (const item of slot(api, value)) {
|
||||||
api.slots.register(item)
|
api.slots.register(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const plugin: TuiPluginModule & { id: string } = {
|
||||||
id: "tui-smoke",
|
id: "tui-smoke",
|
||||||
tui,
|
tui,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
github-policies:
|
|
||||||
runners:
|
|
||||||
allowed_groups:
|
|
||||||
- "GitHub Actions"
|
|
||||||
- "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt"
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.64",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -281,7 +281,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -338,8 +338,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "2.3.3",
|
"@openrouter/ai-sdk-provider": "2.3.3",
|
||||||
"@opentui/core": "0.1.90",
|
"@opentui/core": "0.1.92",
|
||||||
"@opentui/solid": "0.1.90",
|
"@opentui/solid": "0.1.92",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -369,7 +369,7 @@
|
|||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
"opencode-gitlab-auth": "2.0.0",
|
"opencode-gitlab-auth": "2.0.1",
|
||||||
"opencode-poe-auth": "0.0.1",
|
"opencode-poe-auth": "0.0.1",
|
||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.6",
|
||||||
"partial-json": "0.1.7",
|
"partial-json": "0.1.7",
|
||||||
@@ -378,6 +378,7 @@
|
|||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"tree-sitter-bash": "0.25.0",
|
"tree-sitter-bash": "0.25.0",
|
||||||
|
"tree-sitter-powershell": "0.25.10",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"ulid": "catalog:",
|
"ulid": "catalog:",
|
||||||
"vscode-jsonrpc": "8.2.1",
|
"vscode-jsonrpc": "8.2.1",
|
||||||
@@ -422,22 +423,22 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opentui/core": "0.1.90",
|
"@opentui/core": "0.1.92",
|
||||||
"@opentui/solid": "0.1.90",
|
"@opentui/solid": "0.1.92",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.1.90",
|
"@opentui/core": ">=0.1.92",
|
||||||
"@opentui/solid": ">=0.1.90",
|
"@opentui/solid": ">=0.1.92",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -456,7 +457,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.90.10",
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -467,7 +468,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -502,7 +503,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -549,7 +550,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -560,7 +561,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -593,8 +594,9 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"electron",
|
|
||||||
"esbuild",
|
"esbuild",
|
||||||
|
"tree-sitter-powershell",
|
||||||
|
"electron",
|
||||||
"web-tree-sitter",
|
"web-tree-sitter",
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
],
|
],
|
||||||
@@ -610,7 +612,7 @@
|
|||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@effect/platform-node": "4.0.0-beta.37",
|
"@effect/platform-node": "4.0.0-beta.42",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
@@ -634,7 +636,7 @@
|
|||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.37",
|
"effect": "4.0.0-beta.42",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
@@ -993,9 +995,9 @@
|
|||||||
|
|
||||||
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
|
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
|
||||||
|
|
||||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.37", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.37", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.37", "ioredis": "^5.7.0" } }, "sha512-dCfTNYGAT+1K+nu/0jw3FL/0DJXcobZCJs9SD5XJbj1DewWPhR9/AptP6zLGj8vdP8hXem6Aa53nze3HSujW3w=="],
|
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.42", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.42", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.42", "ioredis": "^5.7.0" } }, "sha512-kbdRML2FBa4q8U8rZQcnmLKZ5zN/z1bAA7t5D1/UsBHZqJgnfRgu1CP6kaEfb1Nie6YyaWshxTktZQryjvW/Yg=="],
|
||||||
|
|
||||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.40", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.40" } }, "sha512-WMRVG7T8ZDALKCOacsx2ZZj3Ccaoq8YGeD9q7ZL4q8RwQv8Nmrl+4+KZl95/zHCqXzgK9oUJOlBfQ7CZr6PQOQ=="],
|
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.42", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.42" } }, "sha512-PC+lxLsrwob3+nBChAPrQq32olCeyApgXBvs1NrRsoArLViNT76T/68CttuCAksCZj5e1bZ1ZibLPel3vUmx2g=="],
|
||||||
|
|
||||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||||
|
|
||||||
@@ -1459,21 +1461,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.1.90", "", { "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.90", "@opentui/core-darwin-x64": "0.1.90", "@opentui/core-linux-arm64": "0.1.90", "@opentui/core-linux-x64": "0.1.90", "@opentui/core-win32-arm64": "0.1.90", "@opentui/core-win32-x64": "0.1.90", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Os2dviqWVETU3kaK36lbSvdcI93GAWhw0xb9ng/d0DWYuM9scRmAhLHiOayp61saWv/BR8OJXeuQYHvrp5rd6A=="],
|
"@opentui/core": ["@opentui/core@0.1.92", "", { "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.92", "@opentui/core-darwin-x64": "0.1.92", "@opentui/core-linux-arm64": "0.1.92", "@opentui/core-linux-x64": "0.1.92", "@opentui/core-win32-arm64": "0.1.92", "@opentui/core-win32-x64": "0.1.92", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-c+KdYAIH3M8n24RYaor+t7AQtKZ3l84L7xdP7DEaN4xtuYH8W08E6Gi+wUal4g+HSai3HS9irox68yFf0VPAxw=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.90", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XFrm2zCg1SlHPQ5A2HX/I4dCrmTjYaCJIIpo3QuPIvZBGH3aBMdWDJh2tXw7AB5Mmh8X1K4hDkP5nlK9x0Ewow=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.92", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NX/qFRuc7My0pazyOrw9fdTXmU7omXcZzQuHcsaVnwssljaT52UYMrJ7mCKhSo69RhHw0lnGCymTorvz3XBdsA=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.90", "", { "os": "darwin", "cpu": "x64" }, "sha512-vbDpUsnlZ+0CeVKyBBXE+l2+X1XoVncMxMOhXTiMtud2/Cwu+Vfs/g3LC/6Zv08yaytA+9g7Z8sdf0QCqFyQ4w=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.92", "", { "os": "darwin", "cpu": "x64" }, "sha512-Zb4jn33hOf167llINKLniOabQIycs14LPOBZnQ6l4khbeeTPVJdG8gy9PhlAyIQygDKmRTFncVlP0RP+L6C7og=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.90", "", { "os": "linux", "cpu": "arm64" }, "sha512-OTbvBTP5mVQ4uwKyuz6b59ElG+D0i1Ln+q6cVhNkLgeRLySIn1uXEzUFQGlnVgb8lFDANsn3yQmdv+R+Cpw0og=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.92", "", { "os": "linux", "cpu": "arm64" }, "sha512-4VA1A91OTMPJ3LkAyaxKEZVJsk5jIc3Kz0gV2vip8p2aGLPpYHHpkFZpXP/FyzsnJzoSGftBeA6ya1GKa5bkXg=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.90", "", { "os": "linux", "cpu": "x64" }, "sha512-2PJi/LLlO7tGk9Ful/n+6iBdg1RFrA9ibU7wVneE6Z1P0LCYeu7bpwMzea1TXL0eAQWPHsjTs9aPlqPxln0EJw=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.92", "", { "os": "linux", "cpu": "x64" }, "sha512-tr7va8hfKS1uY+TBmulQBoBlwijzJk56K/U/L9/tbHfW7oJctqxPVwEFHIh1HDcOQ3/UhMMWGvMfeG6cFiK8/A=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.90", "", { "os": "win32", "cpu": "arm64" }, "sha512-+sTRaOb7gCMZ6iLuuG4y9kzyweJzBDcIJN0Xh49ikFWTwVECDXEVtXahNGlw57avm2yYUoNzmpBjK/LV7zBj9A=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.92", "", { "os": "win32", "cpu": "arm64" }, "sha512-34YM3uPtDjzUVeSnJWIK2J8mxyduzV7f3mYc4Hub0glNpUdM1jjzF2HvvvnrKK5ElzTsIcno3c3lOYT8yvG1Zg=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.90", "", { "os": "win32", "cpu": "x64" }, "sha512-aVFyErckWp4oW9NJ/ZDKBUAlTlfVUiRXGP63JXFOoeqI7EYaM8uBt6rgZAJuUdFWCN2Q66WRS8Y2mk+0BJwVBg=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.92", "", { "os": "win32", "cpu": "x64" }, "sha512-uk442kA2Vn0mmJHHqk5sPM+Zai/AN9sgl7egekhoEOUx2VK3gxftKsVlx2YVpCHTvTE/S+vnD2WpQaJk2SNjww=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.1.90", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.90", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-zEHDpJOTGS707ts5j4diqoWuFLSqV6yARKl1H0FJkwWOotu+rxCyksL+C0gX0jJUonAw2cjlZ2NNtZY8g78zkg=="],
|
"@opentui/solid": ["@opentui/solid@0.1.92", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.92", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-0Sx1+6zRpmMJ5oDEY0JS9b9+eGd/Q0fPndNllrQNnp7w2FCjpXmvHdBdq+pFI6kFp01MHq2ZOkUU5zX5/9YMSQ=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -2769,7 +2771,7 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"effect": ["effect@4.0.0-beta.37", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AVMXXtb6n62W4uvo1EvT7FJ41HfDvQRX8IY2FGPvfP361dtBArKK2JtE5vmFXTsxkW90WUdvJZYpVATGIzr/BA=="],
|
"effect": ["effect@4.0.0-beta.42", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-c1UrRP+tLzyHb4Fepl8XBDJlLQLkrcMXrRBba441GQRxMbeQ/aIOSFcBwSda1iMJ5l9F0lYc3Bhe33/whrmavQ=="],
|
||||||
|
|
||||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
@@ -3801,7 +3803,7 @@
|
|||||||
|
|
||||||
"opencode": ["opencode@workspace:packages/opencode"],
|
"opencode": ["opencode@workspace:packages/opencode"],
|
||||||
|
|
||||||
"opencode-gitlab-auth": ["opencode-gitlab-auth@2.0.0", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-jmZOOvYIurRScQCtdBqIW5HbP1JbmIiq7UtI7NGgn2vjke46g9d4NVPBg5/ZmFFVIBwZcgyFgJ7b8kGEOR9ujA=="],
|
"opencode-gitlab-auth": ["opencode-gitlab-auth@2.0.1", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-1EMZHdbADLMVaTVLQ6C/V8uVMDr6MP++osj2lmOecowtn46AafP/w6ADkV4AN/ddjA1rob5cWpMuf/iME6DI6A=="],
|
||||||
|
|
||||||
"opencode-poe-auth": ["opencode-poe-auth@0.0.1", "", { "dependencies": { "open": "^10.0.0", "poe-oauth": "*" }, "peerDependencies": { "@opencode-ai/plugin": "*" } }, "sha512-cXqTlS6AXHzo1oBdosnxbT47ZJEZ9WXn050X8Re6wZ1vaNnTpB/l2fMQt90evT7RBK0fB8UjXQUDMKyd7bbiqg=="],
|
"opencode-poe-auth": ["opencode-poe-auth@0.0.1", "", { "dependencies": { "open": "^10.0.0", "poe-oauth": "*" }, "peerDependencies": { "@opencode-ai/plugin": "*" } }, "sha512-cXqTlS6AXHzo1oBdosnxbT47ZJEZ9WXn050X8Re6wZ1vaNnTpB/l2fMQt90evT7RBK0fB8UjXQUDMKyd7bbiqg=="],
|
||||||
|
|
||||||
@@ -4485,6 +4487,8 @@
|
|||||||
|
|
||||||
"tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
|
"tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
|
||||||
|
|
||||||
|
"tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="],
|
||||||
|
|
||||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-4XhUHjgqinKxOeT8K5hGAjpFA2vzOp8QpEg0uYCZwvg=",
|
"x86_64-linux": "sha256-0JgEA54d1ZZ0IWUmxCJP2fnQ2cpmLO25G+hafUbHFLw=",
|
||||||
"aarch64-linux": "sha256-X2YTNOpJocIkWkkfS8RnuDW+tvj4riHs7CXM+cS9iv0=",
|
"aarch64-linux": "sha256-oB4Ptc+MH76MEw0DZodmCCz87qOmbzi26751ZM4DYyE=",
|
||||||
"aarch64-darwin": "sha256-pN0rY+cpdW+6gNWeegVprdmhc2H72OZ9WxKDIs1fvJM=",
|
"aarch64-darwin": "sha256-712rb7B5gTRz1uTx4cJQSrmq9DoBUe+UxbvawYV4XTE=",
|
||||||
"x86_64-darwin": "sha256-l8+Yz/6UfSPJrdgfcqy/L2SvxN2i9Apv2R0B61rpEmw="
|
"x86_64-darwin": "sha256-GRCiEBDDEyVx1et04xqdIEQr3ykRMOBJoQy/xddSsCA="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ let
|
|||||||
in
|
in
|
||||||
stdenvNoCC.mkDerivation {
|
stdenvNoCC.mkDerivation {
|
||||||
pname = "opencode-node_modules";
|
pname = "opencode-node_modules";
|
||||||
version = "${packageJson.version}-${rev}";
|
version = "${packageJson.version}+${lib.replaceString "-" "." rev}";
|
||||||
|
|
||||||
src = lib.fileset.toSource {
|
src = lib.fileset.toSource {
|
||||||
root = ../.;
|
root = ../.;
|
||||||
@@ -54,6 +54,7 @@ stdenvNoCC.mkDerivation {
|
|||||||
--filter '!./' \
|
--filter '!./' \
|
||||||
--filter './packages/opencode' \
|
--filter './packages/opencode' \
|
||||||
--filter './packages/desktop' \
|
--filter './packages/desktop' \
|
||||||
|
--filter './packages/app' \
|
||||||
--frozen-lockfile \
|
--frozen-lockfile \
|
||||||
--ignore-scripts \
|
--ignore-scripts \
|
||||||
--no-progress
|
--no-progress
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
stdenvNoCC,
|
stdenvNoCC,
|
||||||
callPackage,
|
callPackage,
|
||||||
bun,
|
bun,
|
||||||
|
nodejs,
|
||||||
sysctl,
|
sysctl,
|
||||||
makeBinaryWrapper,
|
makeBinaryWrapper,
|
||||||
models-dev,
|
models-dev,
|
||||||
@@ -19,6 +20,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
bun
|
bun
|
||||||
|
nodejs # for patchShebangs node_modules
|
||||||
installShellFiles
|
installShellFiles
|
||||||
makeBinaryWrapper
|
makeBinaryWrapper
|
||||||
models-dev
|
models-dev
|
||||||
@@ -29,6 +31,8 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
runHook preConfigure
|
runHook preConfigure
|
||||||
|
|
||||||
cp -R ${finalAttrs.node_modules}/. .
|
cp -R ${finalAttrs.node_modules}/. .
|
||||||
|
patchShebangs node_modules
|
||||||
|
patchShebangs packages/*/node_modules
|
||||||
|
|
||||||
runHook postConfigure
|
runHook postConfigure
|
||||||
'';
|
'';
|
||||||
|
|||||||
+3
-2
@@ -25,7 +25,7 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@effect/platform-node": "4.0.0-beta.37",
|
"@effect/platform-node": "4.0.0-beta.42",
|
||||||
"@types/bun": "1.3.11",
|
"@types/bun": "1.3.11",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.37",
|
"effect": "4.0.0-beta.42",
|
||||||
"ai": "6.0.138",
|
"ai": "6.0.138",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
@@ -104,6 +104,7 @@
|
|||||||
"protobufjs",
|
"protobufjs",
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
|
"tree-sitter-powershell",
|
||||||
"web-tree-sitter",
|
"web-tree-sitter",
|
||||||
"electron"
|
"electron"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ test("typing a code font with spaces persists and updates CSS variable", async (
|
|||||||
const dialog = await openSettings(page)
|
const dialog = await openSettings(page)
|
||||||
const input = dialog.locator(settingsCodeFontSelector)
|
const input = dialog.locator(settingsCodeFontSelector)
|
||||||
await expect(input).toBeVisible()
|
await expect(input).toBeVisible()
|
||||||
await expect(input).toHaveAttribute("placeholder", "IBM Plex Mono")
|
await expect(input).toHaveAttribute("placeholder", "System Mono")
|
||||||
|
|
||||||
const initialFontFamily = await page.evaluate(() =>
|
const initialFontFamily = await page.evaluate(() =>
|
||||||
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
@@ -167,7 +167,7 @@ test("typing a code font with spaces persists and updates CSS variable", async (
|
|||||||
const initialUIFamily = await page.evaluate(() =>
|
const initialUIFamily = await page.evaluate(() =>
|
||||||
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
)
|
)
|
||||||
expect(initialFontFamily).toContain("IBM Plex Mono")
|
expect(initialFontFamily).toContain("ui-monospace")
|
||||||
|
|
||||||
const next = "Test Mono"
|
const next = "Test Mono"
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ test("typing a code font with spaces persists and updates CSS variable", async (
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
font: next,
|
mono: next,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ test("typing a UI font with spaces persists and updates CSS variable", async ({
|
|||||||
const dialog = await openSettings(page)
|
const dialog = await openSettings(page)
|
||||||
const input = dialog.locator(settingsUIFontSelector)
|
const input = dialog.locator(settingsUIFontSelector)
|
||||||
await expect(input).toBeVisible()
|
await expect(input).toBeVisible()
|
||||||
await expect(input).toHaveAttribute("placeholder", "Inter")
|
await expect(input).toHaveAttribute("placeholder", "System Sans")
|
||||||
|
|
||||||
const initialFontFamily = await page.evaluate(() =>
|
const initialFontFamily = await page.evaluate(() =>
|
||||||
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
@@ -214,7 +214,7 @@ test("typing a UI font with spaces persists and updates CSS variable", async ({
|
|||||||
const initialCodeFamily = await page.evaluate(() =>
|
const initialCodeFamily = await page.evaluate(() =>
|
||||||
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
)
|
)
|
||||||
expect(initialFontFamily).toContain("Inter")
|
expect(initialFontFamily).toContain("ui-sans-serif")
|
||||||
|
|
||||||
const next = "Test Sans"
|
const next = "Test Sans"
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ test("typing a UI font with spaces persists and updates CSS variable", async ({
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
uiFont: next,
|
sans: next,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -267,14 +267,14 @@ test("clearing the code font field restores the default placeholder and stack",
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
font: "Reset Mono",
|
mono: "Reset Mono",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await input.clear()
|
await input.clear()
|
||||||
await input.press("Space")
|
await input.press("Space")
|
||||||
await expect(input).toHaveValue("")
|
await expect(input).toHaveValue("")
|
||||||
await expect(input).toHaveAttribute("placeholder", "IBM Plex Mono")
|
await expect(input).toHaveAttribute("placeholder", "System Mono")
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => {
|
.poll(async () => {
|
||||||
@@ -285,14 +285,14 @@ test("clearing the code font field restores the default placeholder and stack",
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
font: "",
|
mono: "",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const fontFamily = await page.evaluate(() =>
|
const fontFamily = await page.evaluate(() =>
|
||||||
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
)
|
)
|
||||||
expect(fontFamily).toContain("IBM Plex Mono")
|
expect(fontFamily).toContain("ui-monospace")
|
||||||
expect(fontFamily).not.toContain("Reset Mono")
|
expect(fontFamily).not.toContain("Reset Mono")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -316,14 +316,14 @@ test("clearing the UI font field restores the default placeholder and stack", as
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
uiFont: "Reset Sans",
|
sans: "Reset Sans",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await input.clear()
|
await input.clear()
|
||||||
await input.press("Space")
|
await input.press("Space")
|
||||||
await expect(input).toHaveValue("")
|
await expect(input).toHaveValue("")
|
||||||
await expect(input).toHaveAttribute("placeholder", "Inter")
|
await expect(input).toHaveAttribute("placeholder", "System Sans")
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => {
|
.poll(async () => {
|
||||||
@@ -334,14 +334,14 @@ test("clearing the UI font field restores the default placeholder and stack", as
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
uiFont: "",
|
sans: "",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const fontFamily = await page.evaluate(() =>
|
const fontFamily = await page.evaluate(() =>
|
||||||
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
)
|
)
|
||||||
expect(fontFamily).toContain("Inter")
|
expect(fontFamily).toContain("ui-sans-serif")
|
||||||
expect(fontFamily).not.toContain("Reset Sans")
|
expect(fontFamily).not.toContain("Reset Sans")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -373,8 +373,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
|
|||||||
return raw ? JSON.parse(raw) : null
|
return raw ? JSON.parse(raw) : null
|
||||||
}, settingsKey)
|
}, settingsKey)
|
||||||
|
|
||||||
const mono = initialSettings?.appearance?.font === "Reload Mono" ? "Reload Mono 2" : "Reload Mono"
|
const mono = initialSettings?.appearance?.mono === "Reload Mono" ? "Reload Mono 2" : "Reload Mono"
|
||||||
const sans = initialSettings?.appearance?.uiFont === "Reload Sans" ? "Reload Sans 2" : "Reload Sans"
|
const sans = initialSettings?.appearance?.sans === "Reload Sans" ? "Reload Sans 2" : "Reload Sans"
|
||||||
|
|
||||||
await code.click()
|
await code.click()
|
||||||
await code.clear()
|
await code.clear()
|
||||||
@@ -395,8 +395,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
font: mono,
|
mono,
|
||||||
uiFont: sans,
|
sans,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -415,8 +415,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
|
|||||||
expect(updatedMono).not.toBe(initialMono)
|
expect(updatedMono).not.toBe(initialMono)
|
||||||
expect(updatedSans).toContain(sans)
|
expect(updatedSans).toContain(sans)
|
||||||
expect(updatedSans).not.toBe(initialSans)
|
expect(updatedSans).not.toBe(initialSans)
|
||||||
expect(updatedSettings?.appearance?.font).toBe(mono)
|
expect(updatedSettings?.appearance?.mono).toBe(mono)
|
||||||
expect(updatedSettings?.appearance?.uiFont).toBe(sans)
|
expect(updatedSettings?.appearance?.sans).toBe(sans)
|
||||||
|
|
||||||
await closeDialog(page, dialog)
|
await closeDialog(page, dialog)
|
||||||
await page.reload()
|
await page.reload()
|
||||||
@@ -432,8 +432,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
font: mono,
|
mono,
|
||||||
uiFont: sans,
|
sans,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -468,8 +468,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
|
|||||||
expect(rehydratedMono).not.toBe(initialMono)
|
expect(rehydratedMono).not.toBe(initialMono)
|
||||||
expect(rehydratedSans).toContain(sans)
|
expect(rehydratedSans).toContain(sans)
|
||||||
expect(rehydratedSans).not.toBe(initialSans)
|
expect(rehydratedSans).not.toBe(initialSans)
|
||||||
expect(rehydratedSettings?.appearance?.font).toBe(mono)
|
expect(rehydratedSettings?.appearance?.mono).toBe(mono)
|
||||||
expect(rehydratedSettings?.appearance?.uiFont).toBe(sans)
|
expect(rehydratedSettings?.appearance?.sans).toBe(sans)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
|
test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
var key = "opencode-theme-id"
|
var key = "opencode-theme-id"
|
||||||
var themeId = localStorage.getItem(key) || "oc-2"
|
var themeId = localStorage.getItem(key) || "oc-2"
|
||||||
|
|
||||||
if (themeId === "oc-1") {
|
if (themeId.slice(0, 3) === "oc-" && themeId !== "oc-2") {
|
||||||
themeId = "oc-2"
|
themeId = "oc-2"
|
||||||
localStorage.setItem(key, themeId)
|
localStorage.setItem(key, themeId)
|
||||||
localStorage.removeItem("opencode-theme-css-light")
|
localStorage.removeItem("opencode-theme-css-light")
|
||||||
|
|||||||
@@ -47,9 +47,14 @@ import { ErrorPage } from "./pages/error"
|
|||||||
import { useCheckServerHealth } from "./utils/server-health"
|
import { useCheckServerHealth } from "./utils/server-health"
|
||||||
|
|
||||||
const HomeRoute = lazy(() => import("@/pages/home"))
|
const HomeRoute = lazy(() => import("@/pages/home"))
|
||||||
const Session = lazy(() => import("@/pages/session"))
|
const loadSession = () => import("@/pages/session")
|
||||||
|
const Session = lazy(loadSession)
|
||||||
const Loading = () => <div class="size-full" />
|
const Loading = () => <div class="size-full" />
|
||||||
|
|
||||||
|
if (typeof location === "object" && /\/session(?:\/|$)/.test(location.pathname)) {
|
||||||
|
void loadSession()
|
||||||
|
}
|
||||||
|
|
||||||
const SessionRoute = () => (
|
const SessionRoute = () => (
|
||||||
<SessionProviders>
|
<SessionProviders>
|
||||||
<Session />
|
<Session />
|
||||||
@@ -178,7 +183,7 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
|
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
|
||||||
Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }),
|
Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }),
|
||||||
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
||||||
Effect.runPromise,
|
Effect.runPromise,
|
||||||
),
|
),
|
||||||
@@ -278,7 +283,11 @@ export function AppInterface(props: {
|
|||||||
disableHealthCheck?: boolean
|
disableHealthCheck?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
<ServerProvider
|
||||||
|
defaultServer={props.defaultServer}
|
||||||
|
disableHealthCheck={props.disableHealthCheck}
|
||||||
|
servers={props.servers}
|
||||||
|
>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<ServerKey>
|
<ServerKey>
|
||||||
<GlobalSDKProvider>
|
<GlobalSDKProvider>
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import {
|
|||||||
prependHistoryEntry,
|
prependHistoryEntry,
|
||||||
type PromptHistoryComment,
|
type PromptHistoryComment,
|
||||||
type PromptHistoryEntry,
|
type PromptHistoryEntry,
|
||||||
type PromptHistoryStoredEntry,
|
|
||||||
promptLength,
|
promptLength,
|
||||||
} from "./prompt-input/history"
|
} from "./prompt-input/history"
|
||||||
import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit"
|
import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit"
|
||||||
@@ -313,17 +312,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [history, setHistory] = persisted(
|
const [history, setHistory] = persisted(
|
||||||
Persist.global("prompt-history", ["prompt-history.v1"]),
|
Persist.global("prompt-history.v2"),
|
||||||
createStore<{
|
createStore<{
|
||||||
entries: PromptHistoryStoredEntry[]
|
entries: PromptHistoryEntry[]
|
||||||
}>({
|
}>({
|
||||||
entries: [],
|
entries: [],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const [shellHistory, setShellHistory] = persisted(
|
const [shellHistory, setShellHistory] = persisted(
|
||||||
Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]),
|
Persist.global("prompt-history-shell.v2"),
|
||||||
createStore<{
|
createStore<{
|
||||||
entries: PromptHistoryStoredEntry[]
|
entries: PromptHistoryEntry[]
|
||||||
}>({
|
}>({
|
||||||
entries: [],
|
entries: [],
|
||||||
}),
|
}),
|
||||||
@@ -624,17 +623,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (!cmd) return
|
if (!cmd) return
|
||||||
promptProbe.select(cmd.id)
|
promptProbe.select(cmd.id)
|
||||||
closePopover()
|
closePopover()
|
||||||
|
const images = imageAttachments()
|
||||||
|
|
||||||
if (cmd.type === "custom") {
|
if (cmd.type === "custom") {
|
||||||
const text = `/${cmd.trigger} `
|
const text = `/${cmd.trigger} `
|
||||||
setEditorText(text)
|
setEditorText(text)
|
||||||
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
|
prompt.set([{ type: "text", content: text, start: 0, end: text.length }, ...images], text.length)
|
||||||
focusEditorEnd()
|
focusEditorEnd()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEditor()
|
clearEditor()
|
||||||
prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
|
prompt.set([...DEFAULT_PROMPT, ...images], 0)
|
||||||
command.trigger(cmd.id, "slash")
|
command.trigger(cmd.id, "slash")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Prompt } from "@/context/prompt"
|
|||||||
import {
|
import {
|
||||||
canNavigateHistoryAtCursor,
|
canNavigateHistoryAtCursor,
|
||||||
clonePromptParts,
|
clonePromptParts,
|
||||||
normalizePromptHistoryEntry,
|
|
||||||
navigatePromptHistory,
|
navigatePromptHistory,
|
||||||
prependHistoryEntry,
|
prependHistoryEntry,
|
||||||
promptLength,
|
promptLength,
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||||
|
|
||||||
const text = (value: string): Prompt => [{ type: "text", content: value, start: 0, end: value.length }]
|
const text = (value: string): Prompt => [{ type: "text", content: value, start: 0, end: value.length }]
|
||||||
|
const entry = (value: string, comments: PromptHistoryComment[] = []) => ({ prompt: text(value), comments })
|
||||||
const comment = (id: string, value = "note"): PromptHistoryComment => ({
|
const comment = (id: string, value = "note"): PromptHistoryComment => ({
|
||||||
id,
|
id,
|
||||||
path: "src/a.ts",
|
path: "src/a.ts",
|
||||||
@@ -42,7 +42,7 @@ describe("prompt-input history", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("navigatePromptHistory restores saved prompt when moving down from newest", () => {
|
test("navigatePromptHistory restores saved prompt when moving down from newest", () => {
|
||||||
const entries = [text("third"), text("second"), text("first")]
|
const entries = [entry("third"), entry("second"), entry("first")]
|
||||||
const up = navigatePromptHistory({
|
const up = navigatePromptHistory({
|
||||||
direction: "up",
|
direction: "up",
|
||||||
entries,
|
entries,
|
||||||
@@ -73,12 +73,7 @@ describe("prompt-input history", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("navigatePromptHistory keeps entry comments when moving through history", () => {
|
test("navigatePromptHistory keeps entry comments when moving through history", () => {
|
||||||
const entries = [
|
const entries = [entry("with comment", [comment("c1")])]
|
||||||
{
|
|
||||||
prompt: text("with comment"),
|
|
||||||
comments: [comment("c1")],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const up = navigatePromptHistory({
|
const up = navigatePromptHistory({
|
||||||
direction: "up",
|
direction: "up",
|
||||||
@@ -95,12 +90,6 @@ describe("prompt-input history", () => {
|
|||||||
expect(up.entry.comments).toEqual([comment("c1")])
|
expect(up.entry.comments).toEqual([comment("c1")])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("normalizePromptHistoryEntry supports legacy prompt arrays", () => {
|
|
||||||
const entry = normalizePromptHistoryEntry(text("legacy"))
|
|
||||||
expect(entry.prompt[0]?.type === "text" ? entry.prompt[0].content : "").toBe("legacy")
|
|
||||||
expect(entry.comments).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("helpers clone prompt and count text content length", () => {
|
test("helpers clone prompt and count text content length", () => {
|
||||||
const original: Prompt = [
|
const original: Prompt = [
|
||||||
{ type: "text", content: "one", start: 0, end: 3 },
|
{ type: "text", content: "one", start: 0, end: 3 },
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ export type PromptHistoryEntry = {
|
|||||||
comments: PromptHistoryComment[]
|
comments: PromptHistoryComment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptHistoryStoredEntry = Prompt | PromptHistoryEntry
|
|
||||||
|
|
||||||
export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) {
|
export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) {
|
||||||
const position = Math.max(0, Math.min(cursor, text.length))
|
const position = Math.max(0, Math.min(cursor, text.length))
|
||||||
const atStart = position === 0
|
const atStart = position === 0
|
||||||
@@ -59,13 +57,7 @@ export function clonePromptHistoryComments(comments: PromptHistoryComment[]) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizePromptHistoryEntry(entry: PromptHistoryStoredEntry): PromptHistoryEntry {
|
function clonePromptHistoryEntry(entry: PromptHistoryEntry): PromptHistoryEntry {
|
||||||
if (Array.isArray(entry)) {
|
|
||||||
return {
|
|
||||||
prompt: clonePromptParts(entry),
|
|
||||||
comments: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
prompt: clonePromptParts(entry.prompt),
|
prompt: clonePromptParts(entry.prompt),
|
||||||
comments: clonePromptHistoryComments(entry.comments),
|
comments: clonePromptHistoryComments(entry.comments),
|
||||||
@@ -77,7 +69,7 @@ export function promptLength(prompt: Prompt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function prependHistoryEntry(
|
export function prependHistoryEntry(
|
||||||
entries: PromptHistoryStoredEntry[],
|
entries: PromptHistoryEntry[],
|
||||||
prompt: Prompt,
|
prompt: Prompt,
|
||||||
comments: PromptHistoryComment[] = [],
|
comments: PromptHistoryComment[] = [],
|
||||||
max = MAX_HISTORY,
|
max = MAX_HISTORY,
|
||||||
@@ -112,9 +104,7 @@ function isCommentEqual(commentA: PromptHistoryComment, commentB: PromptHistoryC
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistoryStoredEntry) {
|
function isPromptEqual(entryA: PromptHistoryEntry, entryB: PromptHistoryEntry) {
|
||||||
const entryA = normalizePromptHistoryEntry(promptA)
|
|
||||||
const entryB = normalizePromptHistoryEntry(promptB)
|
|
||||||
if (entryA.prompt.length !== entryB.prompt.length) return false
|
if (entryA.prompt.length !== entryB.prompt.length) return false
|
||||||
for (let i = 0; i < entryA.prompt.length; i++) {
|
for (let i = 0; i < entryA.prompt.length; i++) {
|
||||||
const partA = entryA.prompt[i]
|
const partA = entryA.prompt[i]
|
||||||
@@ -149,7 +139,7 @@ function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistory
|
|||||||
|
|
||||||
type HistoryNavInput = {
|
type HistoryNavInput = {
|
||||||
direction: "up" | "down"
|
direction: "up" | "down"
|
||||||
entries: PromptHistoryStoredEntry[]
|
entries: PromptHistoryEntry[]
|
||||||
historyIndex: number
|
historyIndex: number
|
||||||
currentPrompt: Prompt
|
currentPrompt: Prompt
|
||||||
currentComments: PromptHistoryComment[]
|
currentComments: PromptHistoryComment[]
|
||||||
@@ -181,7 +171,7 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input.historyIndex === -1) {
|
if (input.historyIndex === -1) {
|
||||||
const entry = normalizePromptHistoryEntry(input.entries[0])
|
const entry = clonePromptHistoryEntry(input.entries[0])
|
||||||
return {
|
return {
|
||||||
handled: true,
|
handled: true,
|
||||||
historyIndex: 0,
|
historyIndex: 0,
|
||||||
@@ -196,7 +186,7 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
|||||||
|
|
||||||
if (input.historyIndex < input.entries.length - 1) {
|
if (input.historyIndex < input.entries.length - 1) {
|
||||||
const next = input.historyIndex + 1
|
const next = input.historyIndex + 1
|
||||||
const entry = normalizePromptHistoryEntry(input.entries[next])
|
const entry = clonePromptHistoryEntry(input.entries[next])
|
||||||
return {
|
return {
|
||||||
handled: true,
|
handled: true,
|
||||||
historyIndex: next,
|
historyIndex: next,
|
||||||
@@ -215,7 +205,7 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
|||||||
|
|
||||||
if (input.historyIndex > 0) {
|
if (input.historyIndex > 0) {
|
||||||
const next = input.historyIndex - 1
|
const next = input.historyIndex - 1
|
||||||
const entry = normalizePromptHistoryEntry(input.entries[next])
|
const entry = clonePromptHistoryEntry(input.entries[next])
|
||||||
return {
|
return {
|
||||||
handled: true,
|
handled: true,
|
||||||
historyIndex: next,
|
historyIndex: next,
|
||||||
|
|||||||
@@ -167,10 +167,8 @@ export function createCommentSessionForTest(comments: Record<string, LineComment
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createCommentSession(dir: string, id: string | undefined) {
|
function createCommentSession(dir: string, id: string | undefined) {
|
||||||
const legacy = `${dir}/comments${id ? "/" + id : ""}.v1`
|
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.scoped(dir, id, "comments", [legacy]),
|
Persist.scoped(dir, id, "comments"),
|
||||||
createStore<CommentStore>({
|
createStore<CommentStore>({
|
||||||
comments: {},
|
comments: {},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -34,10 +34,8 @@ function equalSelectedLines(a: SelectedLineRange | null | undefined, b: Selected
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createViewSession(dir: string, id: string | undefined) {
|
function createViewSession(dir: string, id: string | undefined) {
|
||||||
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
|
|
||||||
|
|
||||||
const [view, setView, _, ready] = persisted(
|
const [view, setView, _, ready] = persisted(
|
||||||
Persist.scoped(dir, id, "file-view", [legacyViewKey]),
|
Persist.scoped(dir, id, "file-view"),
|
||||||
createStore<{
|
createStore<{
|
||||||
file: Record<string, FileViewState>
|
file: Record<string, FileViewState>
|
||||||
}>({
|
}>({
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
const aborted = (error: unknown) => abortError.safeParse(error).success
|
const aborted = (error: unknown) => abortError.safeParse(error).success
|
||||||
|
|
||||||
let attempt: AbortController | undefined
|
let attempt: AbortController | undefined
|
||||||
|
let run: Promise<void> | undefined
|
||||||
|
let started = false
|
||||||
const HEARTBEAT_TIMEOUT_MS = 15_000
|
const HEARTBEAT_TIMEOUT_MS = 15_000
|
||||||
let lastEventAt = Date.now()
|
let lastEventAt = Date.now()
|
||||||
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
||||||
@@ -121,8 +123,11 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
heartbeat = undefined
|
heartbeat = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
void (async () => {
|
const start = () => {
|
||||||
while (!abort.signal.aborted) {
|
if (started) return run
|
||||||
|
started = true
|
||||||
|
run = (async () => {
|
||||||
|
while (!abort.signal.aborted && started) {
|
||||||
attempt = new AbortController()
|
attempt = new AbortController()
|
||||||
lastEventAt = Date.now()
|
lastEventAt = Date.now()
|
||||||
const onAbort = () => {
|
const onAbort = () => {
|
||||||
@@ -185,14 +190,26 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
clearHeartbeat()
|
clearHeartbeat()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (abort.signal.aborted) return
|
if (abort.signal.aborted || !started) return
|
||||||
await wait(RECONNECT_DELAY_MS)
|
await wait(RECONNECT_DELAY_MS)
|
||||||
}
|
}
|
||||||
})().finally(flush)
|
})().finally(() => {
|
||||||
|
run = undefined
|
||||||
|
flush()
|
||||||
|
})
|
||||||
|
return run
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
started = false
|
||||||
|
attempt?.abort()
|
||||||
|
clearHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
const onVisibility = () => {
|
const onVisibility = () => {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
if (document.visibilityState !== "visible") return
|
if (document.visibilityState !== "visible") return
|
||||||
|
if (!started) return
|
||||||
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
||||||
attempt?.abort()
|
attempt?.abort()
|
||||||
}
|
}
|
||||||
@@ -204,6 +221,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.removeEventListener("visibilitychange", onVisibility)
|
document.removeEventListener("visibilitychange", onVisibility)
|
||||||
}
|
}
|
||||||
|
stop()
|
||||||
abort.abort()
|
abort.abort()
|
||||||
flush()
|
flush()
|
||||||
})
|
})
|
||||||
@@ -217,7 +235,11 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
return {
|
return {
|
||||||
url: currentServer.http.url,
|
url: currentServer.http.url,
|
||||||
client: sdk,
|
client: sdk,
|
||||||
event: emitter,
|
event: {
|
||||||
|
on: emitter.on.bind(emitter),
|
||||||
|
listen: emitter.listen.bind(emitter),
|
||||||
|
start,
|
||||||
|
},
|
||||||
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
||||||
const s = server.current
|
const s = server.current
|
||||||
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
|
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function createGlobalSync() {
|
|||||||
const sessionMeta = new Map<string, { limit: number }>()
|
const sessionMeta = new Map<string, { limit: number }>()
|
||||||
|
|
||||||
const [projectCache, setProjectCache, projectInit] = persisted(
|
const [projectCache, setProjectCache, projectInit] = persisted(
|
||||||
Persist.global("globalSync.project", ["globalSync.project.v1"]),
|
Persist.global("globalSync.project"),
|
||||||
createStore({ value: [] as Project[] }),
|
createStore({ value: [] as Project[] }),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,10 +72,16 @@ function createGlobalSync() {
|
|||||||
let projectWritten = false
|
let projectWritten = false
|
||||||
let bootedAt = 0
|
let bootedAt = 0
|
||||||
let bootingRoot = false
|
let bootingRoot = false
|
||||||
|
let eventFrame: number | undefined
|
||||||
|
let eventTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
active = false
|
active = false
|
||||||
})
|
})
|
||||||
|
onCleanup(() => {
|
||||||
|
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
||||||
|
if (eventTimer !== undefined) clearTimeout(eventTimer)
|
||||||
|
})
|
||||||
|
|
||||||
const cacheProjects = () => {
|
const cacheProjects = () => {
|
||||||
setProjectCache(
|
setProjectCache(
|
||||||
@@ -348,6 +354,20 @@ function createGlobalSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
if (typeof requestAnimationFrame === "function") {
|
||||||
|
eventFrame = requestAnimationFrame(() => {
|
||||||
|
eventFrame = undefined
|
||||||
|
eventTimer = setTimeout(() => {
|
||||||
|
eventTimer = undefined
|
||||||
|
globalSDK.event.start()
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
eventTimer = setTimeout(() => {
|
||||||
|
eventTimer = undefined
|
||||||
|
globalSDK.event.start()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
void bootstrap()
|
void bootstrap()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ function waitForPaint() {
|
|||||||
const timer = setTimeout(finish, 50)
|
const timer = setTimeout(finish, 50)
|
||||||
if (typeof requestAnimationFrame !== "function") return
|
if (typeof requestAnimationFrame !== "function") return
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(() => {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
finish()
|
finish()
|
||||||
|
}, 0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -87,12 +89,6 @@ export async function bootstrapGlobal(input: {
|
|||||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||||
}) {
|
}) {
|
||||||
const fast = [
|
const fast = [
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.path.get().then((x) => {
|
|
||||||
input.setGlobalStore("path", x.data!)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.globalSDK.global.config.get().then((x) => {
|
input.globalSDK.global.config.get().then((x) => {
|
||||||
@@ -108,6 +104,12 @@ export async function bootstrapGlobal(input: {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const slow = [
|
const slow = [
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.path.get().then((x) => {
|
||||||
|
input.setGlobalStore("path", x.data!)
|
||||||
|
}),
|
||||||
|
),
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.globalSDK.project.list().then((x) => {
|
input.globalSDK.project.list().then((x) => {
|
||||||
@@ -221,12 +223,16 @@ export async function bootstrapDirectory(input: {
|
|||||||
if (loading) input.setStore("status", "partial")
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
const fast = [
|
const fast = [
|
||||||
|
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
|
||||||
|
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||||
|
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||||
|
]
|
||||||
|
|
||||||
|
const slow = [
|
||||||
() =>
|
() =>
|
||||||
seededProject
|
seededProject
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||||
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
|
|
||||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
|
||||||
() =>
|
() =>
|
||||||
seededPath
|
seededPath
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
@@ -237,7 +243,6 @@ export async function bootstrapDirectory(input: {
|
|||||||
if (next) input.setStore("project", next)
|
if (next) input.setStore("project", next)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.sdk.vcs.get().then((x) => {
|
input.sdk.vcs.get().then((x) => {
|
||||||
@@ -299,9 +304,6 @@ export async function bootstrapDirectory(input: {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
]
|
|
||||||
|
|
||||||
const slow = [
|
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
|
|||||||
@@ -125,10 +125,7 @@ export function createChildStoreManager(input: {
|
|||||||
if (!directory) console.error("No directory provided")
|
if (!directory) console.error("No directory provided")
|
||||||
if (!children[directory]) {
|
if (!children[directory]) {
|
||||||
const vcs = runWithOwner(input.owner, () =>
|
const vcs = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(Persist.workspace(directory, "vcs"), createStore({ value: undefined as VcsInfo | undefined })),
|
||||||
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
|
||||||
createStore({ value: undefined as VcsInfo | undefined }),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
|
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
|
||||||
const vcsStore = vcs[0]
|
const vcsStore = vcs[0]
|
||||||
@@ -136,7 +133,7 @@ export function createChildStoreManager(input: {
|
|||||||
|
|
||||||
const meta = runWithOwner(input.owner, () =>
|
const meta = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
Persist.workspace(directory, "project", ["project.v1"]),
|
Persist.workspace(directory, "project"),
|
||||||
createStore({ value: undefined as ProjectMeta | undefined }),
|
createStore({ value: undefined as ProjectMeta | undefined }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -144,10 +141,7 @@ export function createChildStoreManager(input: {
|
|||||||
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
||||||
|
|
||||||
const icon = runWithOwner(input.owner, () =>
|
const icon = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(Persist.workspace(directory, "icon"), createStore({ value: undefined as string | undefined })),
|
||||||
Persist.workspace(directory, "icon", ["icon.v1"]),
|
|
||||||
createStore({ value: undefined as string | undefined }),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
|
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
|
||||||
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
|||||||
init: (props: { locale?: Locale }) => {
|
init: (props: { locale?: Locale }) => {
|
||||||
const initial = props.locale ?? readStoredLocale() ?? detectLocale()
|
const initial = props.locale ?? readStoredLocale() ?? detectLocale()
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("language", ["language.v1"]),
|
Persist.global("language"),
|
||||||
createStore({
|
createStore({
|
||||||
locale: initial,
|
locale: initial,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -123,14 +123,6 @@ const normalizeSessionTabList = (path: ReturnType<typeof createPathHelpers> | un
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
|
|
||||||
const path = sessionPath(key)
|
|
||||||
return {
|
|
||||||
all: normalizeSessionTabList(path, tabs.all),
|
|
||||||
active: tabs.active ? normalizeSessionTab(path, tabs.active) : tabs.active,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
||||||
name: "Layout",
|
name: "Layout",
|
||||||
init: () => {
|
init: () => {
|
||||||
@@ -138,96 +130,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
||||||
|
|
||||||
const migrate = (value: unknown) => {
|
|
||||||
if (!isRecord(value)) return value
|
|
||||||
|
|
||||||
const sidebar = value.sidebar
|
|
||||||
const migratedSidebar = (() => {
|
|
||||||
if (!isRecord(sidebar)) return sidebar
|
|
||||||
if (typeof sidebar.workspaces !== "boolean") return sidebar
|
|
||||||
return {
|
|
||||||
...sidebar,
|
|
||||||
workspaces: {},
|
|
||||||
workspacesDefault: sidebar.workspaces,
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const review = value.review
|
|
||||||
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 : DEFAULT_FILE_TREE_WIDTH
|
|
||||||
return {
|
|
||||||
...fileTree,
|
|
||||||
opened: true,
|
|
||||||
width: width === 260 ? DEFAULT_FILE_TREE_WIDTH : width,
|
|
||||||
tab: "changes",
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const migratedReview = (() => {
|
|
||||||
if (!isRecord(review)) return review
|
|
||||||
if (typeof review.panelOpened === "boolean") return review
|
|
||||||
|
|
||||||
const opened = isRecord(fileTree) && typeof fileTree.opened === "boolean" ? fileTree.opened : true
|
|
||||||
return {
|
|
||||||
...review,
|
|
||||||
panelOpened: opened,
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const sessionTabs = value.sessionTabs
|
|
||||||
const migratedSessionTabs = (() => {
|
|
||||||
if (!isRecord(sessionTabs)) return sessionTabs
|
|
||||||
|
|
||||||
let changed = false
|
|
||||||
const next = Object.fromEntries(
|
|
||||||
Object.entries(sessionTabs).map(([key, tabs]) => {
|
|
||||||
if (!isRecord(tabs) || !Array.isArray(tabs.all)) return [key, tabs]
|
|
||||||
|
|
||||||
const current = {
|
|
||||||
all: tabs.all.filter((tab): tab is string => typeof tab === "string"),
|
|
||||||
active: typeof tabs.active === "string" ? tabs.active : undefined,
|
|
||||||
}
|
|
||||||
const normalized = normalizeStoredSessionTabs(key, current)
|
|
||||||
if (current.all.length !== tabs.all.length) changed = true
|
|
||||||
if (!same(current.all, normalized.all) || current.active !== normalized.active) changed = true
|
|
||||||
if (tabs.active !== undefined && typeof tabs.active !== "string") changed = true
|
|
||||||
return [key, normalized]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!changed) return sessionTabs
|
|
||||||
return next
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (
|
|
||||||
migratedSidebar === sidebar &&
|
|
||||||
migratedReview === review &&
|
|
||||||
migratedFileTree === fileTree &&
|
|
||||||
migratedSessionTabs === sessionTabs
|
|
||||||
) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
sidebar: migratedSidebar,
|
|
||||||
review: migratedReview,
|
|
||||||
fileTree: migratedFileTree,
|
|
||||||
sessionTabs: migratedSessionTabs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = Persist.global("layout", ["layout.v6"])
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
{ ...target, migrate },
|
Persist.global("layout"),
|
||||||
createStore({
|
createStore({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
opened: false,
|
opened: false,
|
||||||
@@ -270,11 +174,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
used: new Map<string, number>(),
|
used: new Map<string, number>(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const SESSION_STATE_KEYS = [
|
const SESSION_STATE_KEYS = ["comments", "file-view", "prompt", "terminal"] as const
|
||||||
{ key: "prompt", legacy: "prompt", version: "v2" },
|
|
||||||
{ key: "terminal", legacy: "terminal", version: "v1" },
|
|
||||||
{ key: "file-view", legacy: "file", version: "v1" },
|
|
||||||
] as const
|
|
||||||
|
|
||||||
const dropSessionState = (keys: string[]) => {
|
const dropSessionState = (keys: string[]) => {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
@@ -283,12 +183,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
const session = parts[1]
|
const session = parts[1]
|
||||||
if (!dir) continue
|
if (!dir) continue
|
||||||
|
|
||||||
for (const entry of SESSION_STATE_KEYS) {
|
for (const item of SESSION_STATE_KEYS) {
|
||||||
const target = session ? Persist.session(dir, session, entry.key) : Persist.workspace(dir, entry.key)
|
void removePersisted(session ? Persist.session(dir, session, item) : Persist.workspace(dir, item), platform)
|
||||||
void removePersisted(target, platform)
|
|
||||||
|
|
||||||
const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}`
|
|
||||||
void removePersisted({ key: legacyKey }, platform)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -544,12 +440,26 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let sessionFrame: number | undefined
|
||||||
|
let sessionTimer: number | undefined
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
Promise.all(
|
sessionFrame = requestAnimationFrame(() => {
|
||||||
|
sessionFrame = undefined
|
||||||
|
sessionTimer = window.setTimeout(() => {
|
||||||
|
sessionTimer = undefined
|
||||||
|
void Promise.all(
|
||||||
server.projects.list().map((project) => {
|
server.projects.list().map((project) => {
|
||||||
return globalSync.project.loadSessions(project.worktree)
|
return globalSync.project.loadSessions(project.worktree)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (sessionFrame !== undefined) cancelAnimationFrame(sessionFrame)
|
||||||
|
if (sessionTimer !== undefined) window.clearTimeout(sessionTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -862,7 +772,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
tabs(sessionKey: string | Accessor<string>) {
|
tabs(sessionKey: string | Accessor<string>) {
|
||||||
const key = createSessionKeyReader(sessionKey, ensureKey)
|
const key = createSessionKeyReader(sessionKey, ensureKey)
|
||||||
const path = createMemo(() => sessionPath(key()))
|
const path = createMemo(() => sessionPath(key()))
|
||||||
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
const tabs = createMemo<SessionTabs>(() => {
|
||||||
|
const current = store.sessionTabs[key()]
|
||||||
|
if (!current || !Array.isArray(current.all)) return { all: [] }
|
||||||
|
return {
|
||||||
|
all: normalizeSessionTabList(
|
||||||
|
path(),
|
||||||
|
current.all.filter((tab): tab is string => typeof tab === "string"),
|
||||||
|
),
|
||||||
|
active: typeof current.active === "string" ? normalizeSessionTab(path(), current.active) : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
const normalize = (tab: string) => normalizeSessionTab(path(), tab)
|
const normalize = (tab: string) => normalizeSessionTab(path(), tab)
|
||||||
const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all)
|
const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all)
|
||||||
return {
|
return {
|
||||||
@@ -889,13 +809,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
async open(tab: string) {
|
async open(tab: string) {
|
||||||
const session = key()
|
const session = key()
|
||||||
const next = nextSessionTabsForOpen(store.sessionTabs[session], normalize(tab))
|
const next = nextSessionTabsForOpen(store.sessionTabs[session] ? tabs() : undefined, normalize(tab))
|
||||||
setStore("sessionTabs", session, next)
|
setStore("sessionTabs", session, next)
|
||||||
},
|
},
|
||||||
close(tab: string) {
|
close(tab: string) {
|
||||||
const session = key()
|
const session = key()
|
||||||
const current = store.sessionTabs[session]
|
if (!store.sessionTabs[session]) return
|
||||||
if (!current) return
|
const current = tabs()
|
||||||
|
|
||||||
if (tab === "review") {
|
if (tab === "review") {
|
||||||
if (current.active !== tab) return
|
if (current.active !== tab) return
|
||||||
@@ -918,8 +838,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
move(tab: string, to: number) {
|
move(tab: string, to: number) {
|
||||||
const session = key()
|
const session = key()
|
||||||
const current = store.sessionTabs[session]
|
if (!store.sessionTabs[session]) return
|
||||||
if (!current) return
|
const current = tabs()
|
||||||
const index = current.all.findIndex((f) => f === tab)
|
const index = current.all.findIndex((f) => f === tab)
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
setStore(
|
setStore(
|
||||||
|
|||||||
@@ -23,27 +23,10 @@ type Saved = {
|
|||||||
session: Record<string, State | undefined>
|
session: Record<string, State | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
const WORKSPACE_KEY = "__workspace__"
|
|
||||||
const handoff = new Map<string, State>()
|
const handoff = new Map<string, State>()
|
||||||
|
|
||||||
const handoffKey = (dir: string, id: string) => `${dir}\n${id}`
|
const handoffKey = (dir: string, id: string) => `${dir}\n${id}`
|
||||||
|
|
||||||
const migrate = (value: unknown) => {
|
|
||||||
if (!value || typeof value !== "object") return { session: {} }
|
|
||||||
|
|
||||||
const item = value as {
|
|
||||||
session?: Record<string, State | undefined>
|
|
||||||
pick?: Record<string, State | undefined>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.session && typeof item.session === "object") return { session: item.session }
|
|
||||||
if (!item.pick || typeof item.pick !== "object") return { session: {} }
|
|
||||||
|
|
||||||
return {
|
|
||||||
session: Object.fromEntries(Object.entries(item.pick).filter(([key]) => key !== WORKSPACE_KEY)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clone = (value: State | undefined) => {
|
const clone = (value: State | undefined) => {
|
||||||
if (!value) return undefined
|
if (!value) return undefined
|
||||||
return {
|
return {
|
||||||
@@ -66,10 +49,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
||||||
|
|
||||||
const [saved, setSaved] = persisted(
|
const [saved, setSaved] = persisted(
|
||||||
{
|
Persist.workspace(sdk.directory, "model-selection"),
|
||||||
...Persist.workspace(sdk.directory, "model-selection", ["model-selection.v1"]),
|
|
||||||
migrate,
|
|
||||||
},
|
|
||||||
createStore<Saved>({
|
createStore<Saved>({
|
||||||
session: {},
|
session: {},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext(
|
|||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("model", ["model.v1"]),
|
Persist.global("model"),
|
||||||
createStore<Store>({
|
createStore<Store>({
|
||||||
user: [],
|
user: [],
|
||||||
recent: [],
|
recent: [],
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
const currentSession = createMemo(() => params.id)
|
const currentSession = createMemo(() => params.id)
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("notification", ["notification.v1"]),
|
Persist.global("notification"),
|
||||||
createStore({
|
createStore({
|
||||||
list: [] as Notification[],
|
list: [] as Notification[],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -25,12 +25,6 @@ describe("autoRespondsPermission", () => {
|
|||||||
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
|
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses a parent session's legacy auto-accept key", () => {
|
|
||||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
|
||||||
|
|
||||||
expect(autoRespondsPermission({ root: true }, sessions, permission("child"), "/tmp/project")).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("defaults to requiring approval when no lineage override exists", () => {
|
test("defaults to requiring approval when no lineage override exists", () => {
|
||||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" }), session({ id: "other" })]
|
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" }), session({ id: "other" })]
|
||||||
const autoAccept = {
|
const autoAccept = {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function directoryAcceptKey(directory: string) {
|
|||||||
function accepted(autoAccept: Record<string, boolean>, sessionID: string, directory?: string) {
|
function accepted(autoAccept: Record<string, boolean>, sessionID: string, directory?: string) {
|
||||||
const key = acceptKey(sessionID, directory)
|
const key = acceptKey(sessionID, directory)
|
||||||
const directoryKey = directory ? directoryAcceptKey(directory) : undefined
|
const directoryKey = directory ? directoryAcceptKey(directory) : undefined
|
||||||
return autoAccept[key] ?? autoAccept[sessionID] ?? (directoryKey ? autoAccept[directoryKey] : undefined)
|
return autoAccept[key] ?? (directoryKey ? autoAccept[directoryKey] : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDirectoryAutoAccepting(autoAccept: Record<string, boolean>, directory: string) {
|
export function isDirectoryAutoAccepting(autoAccept: Record<string, boolean>, directory: string) {
|
||||||
|
|||||||
@@ -59,23 +59,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
{
|
Persist.global("permission"),
|
||||||
...Persist.global("permission", ["permission.v3"]),
|
|
||||||
migrate(value) {
|
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) return value
|
|
||||||
|
|
||||||
const data = value as Record<string, unknown>
|
|
||||||
if (data.autoAccept) return value
|
|
||||||
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
autoAccept:
|
|
||||||
typeof data.autoAcceptEdits === "object" && data.autoAcceptEdits && !Array.isArray(data.autoAcceptEdits)
|
|
||||||
? data.autoAcceptEdits
|
|
||||||
: {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
createStore({
|
createStore({
|
||||||
autoAccept: {} as Record<string, boolean>,
|
autoAccept: {} as Record<string, boolean>,
|
||||||
}),
|
}),
|
||||||
@@ -206,7 +190,6 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
draft.autoAccept[key] = true
|
draft.autoAccept[key] = true
|
||||||
delete draft.autoAccept[sessionID]
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,8 +213,6 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
draft.autoAccept[key] = false
|
draft.autoAccept[key] = false
|
||||||
if (!directory) return
|
|
||||||
delete draft.autoAccept[sessionID]
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,10 +162,8 @@ type PromptCacheEntry = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createPromptSession(dir: string, id: string | undefined) {
|
function createPromptSession(dir: string, id: string | undefined) {
|
||||||
const legacy = `${dir}/prompt${id ? "/" + id : ""}.v2`
|
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.scoped(dir, id, "prompt", [legacy]),
|
Persist.scoped(dir, id, "prompt"),
|
||||||
createStore<{
|
createStore<{
|
||||||
prompt: Prompt
|
prompt: Prompt
|
||||||
cursor?: number
|
cursor?: number
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Persist, persisted } from "@/utils/persist"
|
|||||||
import { useCheckServerHealth } from "@/utils/server-health"
|
import { useCheckServerHealth } from "@/utils/server-health"
|
||||||
|
|
||||||
type StoredProject = { worktree: string; expanded: boolean }
|
type StoredProject = { worktree: string; expanded: boolean }
|
||||||
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
|
||||||
const HEALTH_POLL_INTERVAL_MS = 10_000
|
const HEALTH_POLL_INTERVAL_MS = 10_000
|
||||||
|
|
||||||
export function normalizeServerUrl(input: string) {
|
export function normalizeServerUrl(input: string) {
|
||||||
@@ -94,39 +93,31 @@ export namespace ServerConnection {
|
|||||||
|
|
||||||
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
|
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
|
||||||
name: "Server",
|
name: "Server",
|
||||||
init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
|
init: (props: {
|
||||||
|
defaultServer: ServerConnection.Key
|
||||||
|
disableHealthCheck?: boolean
|
||||||
|
servers?: Array<ServerConnection.Any>
|
||||||
|
}) => {
|
||||||
const checkServerHealth = useCheckServerHealth()
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("server", ["server.v3"]),
|
Persist.global("server"),
|
||||||
createStore({
|
createStore({
|
||||||
list: [] as StoredServer[],
|
list: [] as ServerConnection.Http[],
|
||||||
projects: {} as Record<string, StoredProject[]>,
|
projects: {} as Record<string, StoredProject[]>,
|
||||||
lastProject: {} as Record<string, string>,
|
lastProject: {} as Record<string, string>,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
|
|
||||||
|
|
||||||
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
||||||
const servers = [
|
const servers = [
|
||||||
...(props.servers ?? []),
|
...(props.servers ?? []),
|
||||||
...store.list.map((value) =>
|
...store.list.filter(
|
||||||
typeof value === "string"
|
(value): value is ServerConnection.Http => !!value && typeof value === "object" && value.type === "http",
|
||||||
? {
|
|
||||||
type: "http" as const,
|
|
||||||
http: { url: value },
|
|
||||||
}
|
|
||||||
: value,
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
const deduped = new Map(
|
const deduped = new Map(servers.map((value) => [ServerConnection.key(value), value] as const))
|
||||||
servers.map((value) => {
|
|
||||||
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
|
|
||||||
return [ServerConnection.key(conn), conn]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return [...deduped.values()]
|
return [...deduped.values()]
|
||||||
})
|
})
|
||||||
@@ -172,7 +163,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
if (!url_) return
|
if (!url_) return
|
||||||
const conn = { ...input, http: { ...input.http, url: url_ } }
|
const conn = { ...input, http: { ...input.http, url: url_ } }
|
||||||
return batch(() => {
|
return batch(() => {
|
||||||
const existing = store.list.findIndex((x) => url(x) === url_)
|
const existing = store.list.findIndex((x) => x.http.url === url_)
|
||||||
if (existing !== -1) {
|
if (existing !== -1) {
|
||||||
setStore("list", existing, conn)
|
setStore("list", existing, conn)
|
||||||
} else {
|
} else {
|
||||||
@@ -184,12 +175,12 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function remove(key: ServerConnection.Key) {
|
function remove(key: ServerConnection.Key) {
|
||||||
const list = store.list.filter((x) => url(x) !== key)
|
const list = store.list.filter((x) => ServerConnection.key(x) !== key)
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setStore("list", list)
|
setStore("list", list)
|
||||||
if (state.active === key) {
|
if (state.active === key) {
|
||||||
const next = list[0]
|
const next = list[0]
|
||||||
setState("active", next ? ServerConnection.Key.make(url(next)) : props.defaultServer)
|
setState("active", next ? ServerConnection.key(next) : props.defaultServer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -202,6 +193,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
const current_ = current()
|
const current_ = current()
|
||||||
if (!current_) return
|
if (!current_) return
|
||||||
|
|
||||||
|
if (props.disableHealthCheck) {
|
||||||
|
setState("healthy", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
setState("healthy", undefined)
|
setState("healthy", undefined)
|
||||||
onCleanup(startHealthPolling(current_))
|
onCleanup(startHealthPolling(current_))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export interface Settings {
|
|||||||
}
|
}
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: number
|
fontSize: number
|
||||||
font: string
|
mono: string
|
||||||
uiFont: string
|
sans: string
|
||||||
}
|
}
|
||||||
keybinds: Record<string, string>
|
keybinds: Record<string, string>
|
||||||
permissions: {
|
permissions: {
|
||||||
@@ -43,20 +43,18 @@ export interface Settings {
|
|||||||
sounds: SoundSettings
|
sounds: SoundSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
export const monoDefault = "IBM Plex Mono"
|
export const monoDefault = "System Mono"
|
||||||
export const sansDefault = "Inter"
|
export const sansDefault = "System Sans"
|
||||||
|
|
||||||
const monoFallback =
|
const monoFallback =
|
||||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||||
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
||||||
|
|
||||||
const monoBase = `"${monoDefault}", "IBM Plex Mono Fallback", ${monoFallback}`
|
const monoBase = monoFallback
|
||||||
const sansBase = `"${sansDefault}", "Inter Fallback", ${sansFallback}`
|
const sansBase = sansFallback
|
||||||
const monoKey = "ibm-plex-mono"
|
|
||||||
|
|
||||||
function input(font: string | undefined, key?: string) {
|
function input(font: string | undefined) {
|
||||||
if (!font || font === key || !font.trim()) return ""
|
return font ?? ""
|
||||||
return font
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function family(font: string) {
|
function family(font: string) {
|
||||||
@@ -64,14 +62,14 @@ function family(font: string) {
|
|||||||
return `"${font.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
|
return `"${font.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
function stack(font: string | undefined, base: string, key?: string) {
|
function stack(font: string | undefined, base: string) {
|
||||||
const value = input(font, key).trim()
|
const value = font?.trim() ?? ""
|
||||||
if (!value) return base
|
if (!value) return base
|
||||||
return `${family(value)}, ${base}`
|
return `${family(value)}, ${base}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function monoInput(font: string | undefined) {
|
export function monoInput(font: string | undefined) {
|
||||||
return input(font, monoKey)
|
return input(font)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sansInput(font: string | undefined) {
|
export function sansInput(font: string | undefined) {
|
||||||
@@ -79,7 +77,7 @@ export function sansInput(font: string | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function monoFontFamily(font: string | undefined) {
|
export function monoFontFamily(font: string | undefined) {
|
||||||
return stack(font, monoBase, monoKey)
|
return stack(font, monoBase)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sansFontFamily(font: string | undefined) {
|
export function sansFontFamily(font: string | undefined) {
|
||||||
@@ -100,8 +98,8 @@ const defaultSettings: Settings = {
|
|||||||
},
|
},
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
font: "",
|
mono: "",
|
||||||
uiFont: "",
|
sans: "",
|
||||||
},
|
},
|
||||||
keybinds: {},
|
keybinds: {},
|
||||||
permissions: {
|
permissions: {
|
||||||
@@ -134,8 +132,8 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
|
root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.mono))
|
||||||
root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.uiFont))
|
root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.sans))
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -189,13 +187,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setFontSize(value: number) {
|
setFontSize(value: number) {
|
||||||
setStore("appearance", "fontSize", value)
|
setStore("appearance", "fontSize", value)
|
||||||
},
|
},
|
||||||
font: withFallback(() => store.appearance?.font, defaultSettings.appearance.font),
|
font: withFallback(() => store.appearance?.mono, defaultSettings.appearance.mono),
|
||||||
setFont(value: string) {
|
setFont(value: string) {
|
||||||
setStore("appearance", "font", value.trim() ? value : "")
|
setStore("appearance", "mono", value.trim() ? value : "")
|
||||||
},
|
},
|
||||||
uiFont: withFallback(() => store.appearance?.uiFont, defaultSettings.appearance.uiFont),
|
uiFont: withFallback(() => store.appearance?.sans, defaultSettings.appearance.sans),
|
||||||
setUIFont(value: string) {
|
setUIFont(value: string) {
|
||||||
setStore("appearance", "uiFont", value.trim() ? value : "")
|
setStore("appearance", "sans", value.trim() ? value : "")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
keybinds: {
|
keybinds: {
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
|
||||||
|
|
||||||
let getWorkspaceTerminalCacheKey: (dir: string) => string
|
|
||||||
let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
|
|
||||||
let migrateTerminalState: (value: unknown) => unknown
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
mock.module("@solidjs/router", () => ({
|
|
||||||
useNavigate: () => () => undefined,
|
|
||||||
useParams: () => ({}),
|
|
||||||
}))
|
|
||||||
mock.module("@opencode-ai/ui/context", () => ({
|
|
||||||
createSimpleContext: () => ({
|
|
||||||
use: () => undefined,
|
|
||||||
provider: () => undefined,
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
const mod = await import("./terminal")
|
|
||||||
getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
|
|
||||||
getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
|
|
||||||
migrateTerminalState = mod.migrateTerminalState
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("getWorkspaceTerminalCacheKey", () => {
|
|
||||||
test("uses workspace-only directory cache key", () => {
|
|
||||||
expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("getLegacyTerminalStorageKeys", () => {
|
|
||||||
test("keeps workspace storage path when no legacy session id", () => {
|
|
||||||
expect(getLegacyTerminalStorageKeys("/repo")).toEqual(["/repo/terminal.v1"])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("includes legacy session path before workspace path", () => {
|
|
||||||
expect(getLegacyTerminalStorageKeys("/repo", "session-123")).toEqual([
|
|
||||||
"/repo/terminal/session-123.v1",
|
|
||||||
"/repo/terminal.v1",
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("migrateTerminalState", () => {
|
|
||||||
test("drops invalid terminals and restores a valid active terminal", () => {
|
|
||||||
expect(
|
|
||||||
migrateTerminalState({
|
|
||||||
active: "missing",
|
|
||||||
all: [
|
|
||||||
null,
|
|
||||||
{ id: "one", title: "Terminal 2" },
|
|
||||||
{ id: "one", title: "duplicate", titleNumber: 9 },
|
|
||||||
{ id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 },
|
|
||||||
{ title: "no-id" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
active: "one",
|
|
||||||
all: [
|
|
||||||
{ id: "one", title: "Terminal 2", titleNumber: 2 },
|
|
||||||
{ id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("keeps a valid active id", () => {
|
|
||||||
expect(
|
|
||||||
migrateTerminalState({
|
|
||||||
active: "two",
|
|
||||||
all: [
|
|
||||||
{ id: "one", title: "Terminal 1" },
|
|
||||||
{ id: "two", title: "shell", titleNumber: 7 },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
active: "two",
|
|
||||||
all: [
|
|
||||||
{ id: "one", title: "Terminal 1", titleNumber: 1 },
|
|
||||||
{ id: "two", title: "shell", titleNumber: 7 },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -4,7 +4,7 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli
|
|||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import type { Platform } from "./platform"
|
import type { Platform } from "./platform"
|
||||||
import { defaultTitle, titleNumber } from "./terminal-title"
|
import { defaultTitle } from "./terminal-title"
|
||||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||||
|
|
||||||
export type LocalPTY = {
|
export type LocalPTY = {
|
||||||
@@ -33,64 +33,28 @@ function num(value: unknown) {
|
|||||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function numberFromTitle(title: string) {
|
function pty(value: unknown): value is LocalPTY {
|
||||||
return titleNumber(title, MAX_TERMINAL_SESSIONS)
|
if (!record(value)) return false
|
||||||
}
|
|
||||||
|
|
||||||
function pty(value: unknown): LocalPTY | undefined {
|
|
||||||
if (!record(value)) return
|
|
||||||
|
|
||||||
const id = text(value.id)
|
const id = text(value.id)
|
||||||
if (!id) return
|
if (!id) return false
|
||||||
|
|
||||||
const title = text(value.title) ?? ""
|
const title = text(value.title)
|
||||||
const number = num(value.titleNumber)
|
const number = num(value.titleNumber)
|
||||||
const rows = num(value.rows)
|
if (!title) return false
|
||||||
const cols = num(value.cols)
|
if (!number || number <= 0) return false
|
||||||
const buffer = text(value.buffer)
|
if (value.rows !== undefined && num(value.rows) === undefined) return false
|
||||||
const scrollY = num(value.scrollY)
|
if (value.cols !== undefined && num(value.cols) === undefined) return false
|
||||||
const cursor = num(value.cursor)
|
if (value.buffer !== undefined && text(value.buffer) === undefined) return false
|
||||||
|
if (value.scrollY !== undefined && num(value.scrollY) === undefined) return false
|
||||||
return {
|
if (value.cursor !== undefined && num(value.cursor) === undefined) return false
|
||||||
id,
|
return true
|
||||||
title,
|
|
||||||
titleNumber: number && number > 0 ? number : (numberFromTitle(title) ?? 0),
|
|
||||||
...(rows !== undefined ? { rows } : {}),
|
|
||||||
...(cols !== undefined ? { cols } : {}),
|
|
||||||
...(buffer !== undefined ? { buffer } : {}),
|
|
||||||
...(scrollY !== undefined ? { scrollY } : {}),
|
|
||||||
...(cursor !== undefined ? { cursor } : {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function migrateTerminalState(value: unknown) {
|
|
||||||
if (!record(value)) return value
|
|
||||||
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const all = (Array.isArray(value.all) ? value.all : []).flatMap((item) => {
|
|
||||||
const next = pty(item)
|
|
||||||
if (!next || seen.has(next.id)) return []
|
|
||||||
seen.add(next.id)
|
|
||||||
return [next]
|
|
||||||
})
|
|
||||||
|
|
||||||
const active = text(value.active)
|
|
||||||
|
|
||||||
return {
|
|
||||||
active: active && seen.has(active) ? active : all[0]?.id,
|
|
||||||
all,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkspaceTerminalCacheKey(dir: string) {
|
export function getWorkspaceTerminalCacheKey(dir: string) {
|
||||||
return `${dir}:${WORKSPACE_KEY}`
|
return `${dir}:${WORKSPACE_KEY}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) {
|
|
||||||
if (!legacySessionID) return [`${dir}/terminal.v1`]
|
|
||||||
return [`${dir}/terminal/${legacySessionID}.v1`, `${dir}/terminal.v1`]
|
|
||||||
}
|
|
||||||
|
|
||||||
type TerminalSession = ReturnType<typeof createWorkspaceTerminalSession>
|
type TerminalSession = ReturnType<typeof createWorkspaceTerminalSession>
|
||||||
|
|
||||||
type TerminalCacheEntry = {
|
type TerminalCacheEntry = {
|
||||||
@@ -110,7 +74,7 @@ const trimTerminal = (pty: LocalPTY) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) {
|
export function clearWorkspaceTerminals(dir: string, platform?: Platform) {
|
||||||
const key = getWorkspaceTerminalCacheKey(dir)
|
const key = getWorkspaceTerminalCacheKey(dir)
|
||||||
for (const cache of caches) {
|
for (const cache of caches) {
|
||||||
const entry = cache.get(key)
|
const entry = cache.get(key)
|
||||||
@@ -118,26 +82,11 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
|||||||
}
|
}
|
||||||
|
|
||||||
removePersisted(Persist.workspace(dir, "terminal"), platform)
|
removePersisted(Persist.workspace(dir, "terminal"), platform)
|
||||||
|
|
||||||
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
|
||||||
for (const id of sessionIDs ?? []) {
|
|
||||||
for (const key of getLegacyTerminalStorageKeys(dir, id)) {
|
|
||||||
legacy.add(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const key of legacy) {
|
|
||||||
removePersisted({ key }, platform)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, legacySessionID?: string) {
|
function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string) {
|
||||||
const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID)
|
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
{
|
Persist.workspace(dir, "terminal"),
|
||||||
...Persist.workspace(dir, "terminal", legacy),
|
|
||||||
migrate: migrateTerminalState,
|
|
||||||
},
|
|
||||||
createStore<{
|
createStore<{
|
||||||
active?: string
|
active?: string
|
||||||
all: LocalPTY[]
|
all: LocalPTY[]
|
||||||
@@ -146,16 +95,20 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!ready()) return
|
||||||
|
const all = store.all.filter(pty)
|
||||||
|
const active =
|
||||||
|
typeof store.active === "string" && all.some((item) => item.id === store.active) ? store.active : all[0]?.id
|
||||||
|
if (all.length === store.all.length && active === store.active) return
|
||||||
|
batch(() => {
|
||||||
|
setStore("all", all)
|
||||||
|
if (active !== store.active) setStore("active", active)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const pickNextTerminalNumber = () => {
|
const pickNextTerminalNumber = () => {
|
||||||
const existingTitleNumbers = new Set(
|
const existingTitleNumbers = new Set(store.all.flatMap((pty) => (pty.titleNumber > 0 ? [pty.titleNumber] : [])))
|
||||||
store.all.flatMap((pty) => {
|
|
||||||
const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
|
|
||||||
if (direct !== undefined) return [direct]
|
|
||||||
const parsed = numberFromTitle(pty.title)
|
|
||||||
if (parsed === undefined) return []
|
|
||||||
return [parsed]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find(
|
Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find(
|
||||||
@@ -382,7 +335,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadWorkspace = (dir: string, legacySessionID?: string) => {
|
const loadWorkspace = (dir: string) => {
|
||||||
// Terminals are workspace-scoped so tabs persist while switching sessions in the same directory.
|
// Terminals are workspace-scoped so tabs persist while switching sessions in the same directory.
|
||||||
const key = getWorkspaceTerminalCacheKey(dir)
|
const key = getWorkspaceTerminalCacheKey(dir)
|
||||||
const existing = cache.get(key)
|
const existing = cache.get(key)
|
||||||
@@ -393,7 +346,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entry = createRoot((dispose) => ({
|
const entry = createRoot((dispose) => ({
|
||||||
value: createWorkspaceTerminalSession(sdk, dir, legacySessionID),
|
value: createWorkspaceTerminalSession(sdk, dir),
|
||||||
dispose,
|
dispose,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -402,7 +355,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
return entry.value
|
return entry.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id))
|
const workspace = createMemo(() => loadWorkspace(params.dir!))
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
@@ -411,7 +364,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
if (!prev?.dir) return
|
if (!prev?.dir) return
|
||||||
if (next.dir === prev.dir && next.id === prev.id) return
|
if (next.dir === prev.dir && next.id === prev.id) return
|
||||||
if (next.dir === prev.dir && next.id) return
|
if (next.dir === prev.dir && next.id) return
|
||||||
loadWorkspace(prev.dir, prev.id).trimAll()
|
loadWorkspace(prev.dir).trimAll()
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ import { SidebarContent } from "./layout/sidebar-shell"
|
|||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const [store, setStore, , ready] = persisted(
|
const [store, setStore, , ready] = persisted(
|
||||||
Persist.global("layout.page", ["layout.page.v1"]),
|
Persist.global("layout.page"),
|
||||||
createStore({
|
createStore({
|
||||||
lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } },
|
lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } },
|
||||||
activeProject: undefined as string | undefined,
|
activeProject: undefined as string | undefined,
|
||||||
@@ -1567,11 +1567,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
.then((x) => x.data ?? [])
|
.then((x) => x.data ?? [])
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
|
|
||||||
clearWorkspaceTerminals(
|
clearWorkspaceTerminals(directory, platform)
|
||||||
directory,
|
|
||||||
sessions.map((s) => s.id),
|
|
||||||
platform,
|
|
||||||
)
|
|
||||||
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
||||||
|
|
||||||
const result = await globalSDK.client.worktree
|
const result = await globalSDK.client.worktree
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [followup, setFollowup] = persisted(
|
const [followup, setFollowup] = persisted(
|
||||||
Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
|
Persist.workspace(sdk.directory, "followup"),
|
||||||
createStore<{
|
createStore<{
|
||||||
items: Record<string, FollowupItem[] | undefined>
|
items: Record<string, FollowupItem[] | undefined>
|
||||||
failed: Record<string, string | undefined>
|
failed: Record<string, string | undefined>
|
||||||
@@ -544,6 +544,8 @@ export default function Page() {
|
|||||||
let reviewFrame: number | undefined
|
let reviewFrame: number | undefined
|
||||||
let refreshFrame: number | undefined
|
let refreshFrame: number | undefined
|
||||||
let refreshTimer: number | undefined
|
let refreshTimer: number | undefined
|
||||||
|
let todoFrame: number | undefined
|
||||||
|
let todoTimer: number | undefined
|
||||||
let diffFrame: number | undefined
|
let diffFrame: number | undefined
|
||||||
let diffTimer: number | undefined
|
let diffTimer: number | undefined
|
||||||
|
|
||||||
@@ -718,7 +720,6 @@ export default function Page() {
|
|||||||
if (!info) return true
|
if (!info) return true
|
||||||
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
||||||
})()
|
})()
|
||||||
const todos = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined)
|
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
void sync.session.sync(id)
|
void sync.session.sync(id)
|
||||||
})
|
})
|
||||||
@@ -730,13 +731,47 @@ export default function Page() {
|
|||||||
if (params.id !== id) return
|
if (params.id !== id) return
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if (stale) void sync.session.sync(id, { force: true })
|
if (stale) void sync.session.sync(id, { force: true })
|
||||||
void sync.session.todo(id, todos ? { force: true } : undefined)
|
|
||||||
})
|
})
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => {
|
||||||
|
const id = params.id
|
||||||
|
return [
|
||||||
|
sdk.directory,
|
||||||
|
id,
|
||||||
|
id ? (sync.data.session_status[id]?.type ?? "idle") : "idle",
|
||||||
|
id ? composer.blocked() : false,
|
||||||
|
] as const
|
||||||
|
},
|
||||||
|
([dir, id, status, blocked]) => {
|
||||||
|
if (todoFrame !== undefined) cancelAnimationFrame(todoFrame)
|
||||||
|
if (todoTimer !== undefined) window.clearTimeout(todoTimer)
|
||||||
|
todoFrame = undefined
|
||||||
|
todoTimer = undefined
|
||||||
|
if (!id) return
|
||||||
|
if (status === "idle" && !blocked) return
|
||||||
|
const cached = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined)
|
||||||
|
|
||||||
|
todoFrame = requestAnimationFrame(() => {
|
||||||
|
todoFrame = undefined
|
||||||
|
todoTimer = window.setTimeout(() => {
|
||||||
|
todoTimer = undefined
|
||||||
|
if (sdk.directory !== dir || params.id !== id) return
|
||||||
|
untrack(() => {
|
||||||
|
void sync.session.todo(id, cached ? { force: true } : undefined)
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => visibleUserMessages().at(-1)?.id,
|
() => visibleUserMessages().at(-1)?.id,
|
||||||
@@ -1658,6 +1693,8 @@ export default function Page() {
|
|||||||
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
|
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
|
||||||
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
||||||
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
||||||
|
if (todoFrame !== undefined) cancelAnimationFrame(todoFrame)
|
||||||
|
if (todoTimer !== undefined) window.clearTimeout(todoTimer)
|
||||||
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
|
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
|
||||||
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
|
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
|
||||||
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
|
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
|
||||||
|
|||||||
@@ -11,6 +11,47 @@ import { useSDK } from "@/context/sdk"
|
|||||||
|
|
||||||
const cache = new Map<string, { tab: number; answers: QuestionAnswer[]; custom: string[]; customOn: boolean[] }>()
|
const cache = new Map<string, { tab: number; answers: QuestionAnswer[]; custom: string[]; customOn: boolean[] }>()
|
||||||
|
|
||||||
|
function Mark(props: { multi: boolean; picked: boolean; onClick?: (event: MouseEvent) => void }) {
|
||||||
|
return (
|
||||||
|
<span data-slot="question-option-check" aria-hidden="true" onClick={props.onClick}>
|
||||||
|
<span data-slot="question-option-box" data-type={props.multi ? "checkbox" : "radio"} data-picked={props.picked}>
|
||||||
|
<Show when={props.multi} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||||
|
<Icon name="check-small" size="small" />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Option(props: {
|
||||||
|
multi: boolean
|
||||||
|
picked: boolean
|
||||||
|
label: string
|
||||||
|
description?: string
|
||||||
|
disabled: boolean
|
||||||
|
onClick: VoidFunction
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-slot="question-option"
|
||||||
|
data-picked={props.picked}
|
||||||
|
role={props.multi ? "checkbox" : "radio"}
|
||||||
|
aria-checked={props.picked}
|
||||||
|
disabled={props.disabled}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<Mark multi={props.multi} picked={props.picked} />
|
||||||
|
<span data-slot="question-option-main">
|
||||||
|
<span data-slot="option-label">{props.label}</span>
|
||||||
|
<Show when={props.description}>
|
||||||
|
<span data-slot="option-description">{props.description}</span>
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit: () => void }> = (props) => {
|
export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit: () => void }> = (props) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -41,6 +82,9 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
return language.t("session.question.progress", { current: n, total: total() })
|
return language.t("session.question.progress", { current: n, total: total() })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const customLabel = () => language.t("ui.messagePart.option.typeOwnAnswer")
|
||||||
|
const customPlaceholder = () => language.t("ui.question.custom.placeholder")
|
||||||
|
|
||||||
const last = createMemo(() => store.tab >= total() - 1)
|
const last = createMemo(() => store.tab >= total() - 1)
|
||||||
|
|
||||||
const customUpdate = (value: string, selected: boolean = on()) => {
|
const customUpdate = (value: string, selected: boolean = on()) => {
|
||||||
@@ -164,6 +208,13 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
|
|
||||||
const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? []))
|
const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? []))
|
||||||
|
|
||||||
|
const answered = (i: number) => {
|
||||||
|
if ((store.answers[i]?.length ?? 0) > 0) return true
|
||||||
|
return store.customOn[i] === true && (store.custom[i] ?? "").trim().length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const picked = (answer: string) => store.answers[store.tab]?.includes(answer) ?? false
|
||||||
|
|
||||||
const pick = (answer: string, custom: boolean = false) => {
|
const pick = (answer: string, custom: boolean = false) => {
|
||||||
setStore("answers", store.tab, [answer])
|
setStore("answers", store.tab, [answer])
|
||||||
if (custom) setStore("custom", store.tab, answer)
|
if (custom) setStore("custom", store.tab, answer)
|
||||||
@@ -230,6 +281,24 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
customUpdate(input())
|
customUpdate(input())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resizeInput = (el: HTMLTextAreaElement) => {
|
||||||
|
el.style.height = "0px"
|
||||||
|
el.style.height = `${el.scrollHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusCustom = (el: HTMLTextAreaElement) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
el.focus()
|
||||||
|
resizeInput(el)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCustomMark = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
customToggle()
|
||||||
|
}
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (sending()) return
|
if (sending()) return
|
||||||
if (store.editing) commitCustom()
|
if (store.editing) commitCustom()
|
||||||
@@ -270,10 +339,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
type="button"
|
type="button"
|
||||||
data-slot="question-progress-segment"
|
data-slot="question-progress-segment"
|
||||||
data-active={i() === store.tab}
|
data-active={i() === store.tab}
|
||||||
data-answered={
|
data-answered={answered(i())}
|
||||||
(store.answers[i()]?.length ?? 0) > 0 ||
|
|
||||||
(store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
|
|
||||||
}
|
|
||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
onClick={() => jump(i())}
|
onClick={() => jump(i())}
|
||||||
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
||||||
@@ -307,43 +373,23 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
</Show>
|
</Show>
|
||||||
<div data-slot="question-options">
|
<div data-slot="question-options">
|
||||||
<For each={options()}>
|
<For each={options()}>
|
||||||
{(opt, i) => {
|
{(opt, i) => (
|
||||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
<Option
|
||||||
return (
|
multi={multi()}
|
||||||
<button
|
picked={picked(opt.label)}
|
||||||
data-slot="question-option"
|
label={opt.label}
|
||||||
data-picked={picked()}
|
description={opt.description}
|
||||||
role={multi() ? "checkbox" : "radio"}
|
|
||||||
aria-checked={picked()}
|
|
||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
onClick={() => selectOption(i())}
|
onClick={() => selectOption(i())}
|
||||||
>
|
/>
|
||||||
<span data-slot="question-option-check" aria-hidden="true">
|
)}
|
||||||
<span
|
|
||||||
data-slot="question-option-box"
|
|
||||||
data-type={multi() ? "checkbox" : "radio"}
|
|
||||||
data-picked={picked()}
|
|
||||||
>
|
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
|
||||||
<Icon name="check-small" size="small" />
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span data-slot="question-option-main">
|
|
||||||
<span data-slot="option-label">{opt.label}</span>
|
|
||||||
<Show when={opt.description}>
|
|
||||||
<span data-slot="option-description">{opt.description}</span>
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={store.editing}
|
when={store.editing}
|
||||||
fallback={
|
fallback={
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
data-slot="question-option"
|
data-slot="question-option"
|
||||||
data-custom="true"
|
data-custom="true"
|
||||||
data-picked={on()}
|
data-picked={on()}
|
||||||
@@ -352,24 +398,10 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
onClick={customOpen}
|
onClick={customOpen}
|
||||||
>
|
>
|
||||||
<span
|
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
|
||||||
data-slot="question-option-check"
|
|
||||||
aria-hidden="true"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
customToggle()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
|
||||||
<Icon name="check-small" size="small" />
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span data-slot="question-option-main">
|
<span data-slot="question-option-main">
|
||||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
<span data-slot="option-label">{customLabel()}</span>
|
||||||
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
<span data-slot="option-description">{input() || customPlaceholder()}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -394,33 +426,13 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
commitCustom()
|
commitCustom()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
|
||||||
data-slot="question-option-check"
|
|
||||||
aria-hidden="true"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
customToggle()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
|
||||||
<Icon name="check-small" size="small" />
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span data-slot="question-option-main">
|
<span data-slot="question-option-main">
|
||||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
<span data-slot="option-label">{customLabel()}</span>
|
||||||
<textarea
|
<textarea
|
||||||
ref={(el) =>
|
ref={focusCustom}
|
||||||
setTimeout(() => {
|
|
||||||
el.focus()
|
|
||||||
el.style.height = "0px"
|
|
||||||
el.style.height = `${el.scrollHeight}px`
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
data-slot="question-custom-input"
|
data-slot="question-custom-input"
|
||||||
placeholder={language.t("ui.question.custom.placeholder")}
|
placeholder={customPlaceholder()}
|
||||||
value={input()}
|
value={input()}
|
||||||
rows={1}
|
rows={1}
|
||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
@@ -436,8 +448,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}}
|
}}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
customUpdate(e.currentTarget.value)
|
customUpdate(e.currentTarget.value)
|
||||||
e.currentTarget.style.height = "0px"
|
resizeInput(e.currentTarget)
|
||||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -52,6 +52,132 @@ function FileCommentMenu(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScrollPos = { x: number; y: number }
|
||||||
|
|
||||||
|
function createScrollSync(input: { tab: () => string; view: ReturnType<typeof useSessionLayout>["view"] }) {
|
||||||
|
let scroll: HTMLDivElement | undefined
|
||||||
|
let scrollFrame: number | undefined
|
||||||
|
let restoreFrame: number | undefined
|
||||||
|
let pending: ScrollPos | undefined
|
||||||
|
let code: HTMLElement[] = []
|
||||||
|
|
||||||
|
const getCode = () => {
|
||||||
|
const el = scroll
|
||||||
|
if (!el) return []
|
||||||
|
|
||||||
|
const host = el.querySelector("diffs-container")
|
||||||
|
if (!(host instanceof HTMLElement)) return []
|
||||||
|
|
||||||
|
const root = host.shadowRoot
|
||||||
|
if (!root) return []
|
||||||
|
|
||||||
|
return Array.from(root.querySelectorAll("[data-code]")).filter(
|
||||||
|
(node): node is HTMLElement => node instanceof HTMLElement && node.clientWidth > 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = (next: ScrollPos) => {
|
||||||
|
pending = next
|
||||||
|
if (scrollFrame !== undefined) return
|
||||||
|
|
||||||
|
scrollFrame = requestAnimationFrame(() => {
|
||||||
|
scrollFrame = undefined
|
||||||
|
|
||||||
|
const out = pending
|
||||||
|
pending = undefined
|
||||||
|
if (!out) return
|
||||||
|
|
||||||
|
input.view().setScroll(input.tab(), out)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCodeScroll = (event: Event) => {
|
||||||
|
const el = scroll
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const target = event.currentTarget
|
||||||
|
if (!(target instanceof HTMLElement)) return
|
||||||
|
|
||||||
|
save({
|
||||||
|
x: target.scrollLeft,
|
||||||
|
y: el.scrollTop,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sync = () => {
|
||||||
|
const next = getCode()
|
||||||
|
if (next.length === code.length && next.every((el, i) => el === code[i])) return
|
||||||
|
|
||||||
|
for (const item of code) {
|
||||||
|
item.removeEventListener("scroll", onCodeScroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
code = next
|
||||||
|
|
||||||
|
for (const item of code) {
|
||||||
|
item.addEventListener("scroll", onCodeScroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restore = () => {
|
||||||
|
const el = scroll
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const pos = input.view().scroll(input.tab())
|
||||||
|
if (!pos) return
|
||||||
|
|
||||||
|
sync()
|
||||||
|
|
||||||
|
if (code.length > 0) {
|
||||||
|
for (const item of code) {
|
||||||
|
if (item.scrollLeft !== pos.x) item.scrollLeft = pos.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.scrollTop !== pos.y) el.scrollTop = pos.y
|
||||||
|
if (code.length > 0) return
|
||||||
|
if (el.scrollLeft !== pos.x) el.scrollLeft = pos.x
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueRestore = () => {
|
||||||
|
if (restoreFrame !== undefined) return
|
||||||
|
|
||||||
|
restoreFrame = requestAnimationFrame(() => {
|
||||||
|
restoreFrame = undefined
|
||||||
|
restore()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
|
||||||
|
if (code.length === 0) sync()
|
||||||
|
|
||||||
|
save({
|
||||||
|
x: code[0]?.scrollLeft ?? event.currentTarget.scrollLeft,
|
||||||
|
y: event.currentTarget.scrollTop,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setViewport = (el: HTMLDivElement) => {
|
||||||
|
scroll = el
|
||||||
|
restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
for (const item of code) {
|
||||||
|
item.removeEventListener("scroll", onCodeScroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
|
||||||
|
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleScroll,
|
||||||
|
queueRestore,
|
||||||
|
setViewport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function FileTabContent(props: { tab: string }) {
|
export function FileTabContent(props: { tab: string }) {
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
@@ -65,11 +191,6 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||||
}).activeFileTab
|
}).activeFileTab
|
||||||
|
|
||||||
let scroll: HTMLDivElement | undefined
|
|
||||||
let scrollFrame: number | undefined
|
|
||||||
let restoreFrame: number | undefined
|
|
||||||
let pending: { x: number; y: number } | undefined
|
|
||||||
let codeScroll: HTMLElement[] = []
|
|
||||||
let find: FileSearchHandle | null = null
|
let find: FileSearchHandle | null = null
|
||||||
|
|
||||||
const search = {
|
const search = {
|
||||||
@@ -92,6 +213,10 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
if (file.ready()) return (file.selectedLines(p) as SelectedLineRange | undefined) ?? null
|
if (file.ready()) return (file.selectedLines(p) as SelectedLineRange | undefined) ?? null
|
||||||
return (getSessionHandoff(sessionKey())?.files[p] as SelectedLineRange | undefined) ?? null
|
return (getSessionHandoff(sessionKey())?.files[p] as SelectedLineRange | undefined) ?? null
|
||||||
})
|
})
|
||||||
|
const scrollSync = createScrollSync({
|
||||||
|
tab: () => props.tab,
|
||||||
|
view,
|
||||||
|
})
|
||||||
|
|
||||||
const selectionPreview = (source: string, selection: FileSelection) => {
|
const selectionPreview = (source: string, selection: FileSelection) => {
|
||||||
return previewSelectedLines(source, {
|
return previewSelectedLines(source, {
|
||||||
@@ -100,6 +225,12 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildPreview = (filePath: string, selection: FileSelection) => {
|
||||||
|
const source = filePath === path() ? contents() : file.get(filePath)?.content?.content
|
||||||
|
if (!source) return undefined
|
||||||
|
return selectionPreview(source, selection)
|
||||||
|
}
|
||||||
|
|
||||||
const addCommentToContext = (input: {
|
const addCommentToContext = (input: {
|
||||||
file: string
|
file: string
|
||||||
selection: SelectedLineRange
|
selection: SelectedLineRange
|
||||||
@@ -108,14 +239,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
origin?: "review" | "file"
|
origin?: "review" | "file"
|
||||||
}) => {
|
}) => {
|
||||||
const selection = selectionFromLines(input.selection)
|
const selection = selectionFromLines(input.selection)
|
||||||
const preview =
|
const preview = input.preview ?? buildPreview(input.file, selection)
|
||||||
input.preview ??
|
|
||||||
(() => {
|
|
||||||
if (input.file === path()) return selectionPreview(contents(), selection)
|
|
||||||
const source = file.get(input.file)?.content?.content
|
|
||||||
if (!source) return undefined
|
|
||||||
return selectionPreview(source, selection)
|
|
||||||
})()
|
|
||||||
|
|
||||||
const saved = comments.add({
|
const saved = comments.add({
|
||||||
file: input.file,
|
file: input.file,
|
||||||
@@ -140,8 +264,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
comment: string
|
comment: string
|
||||||
}) => {
|
}) => {
|
||||||
comments.update(input.file, input.id, input.comment)
|
comments.update(input.file, input.id, input.comment)
|
||||||
const preview =
|
const preview = input.file === path() ? buildPreview(input.file, selectionFromLines(input.selection)) : undefined
|
||||||
input.file === path() ? selectionPreview(contents(), selectionFromLines(input.selection)) : undefined
|
|
||||||
prompt.context.updateComment(input.file, input.id, {
|
prompt.context.updateComment(input.file, input.id, {
|
||||||
comment: input.comment,
|
comment: input.comment,
|
||||||
...(preview ? { preview } : {}),
|
...(preview ? { preview } : {}),
|
||||||
@@ -260,102 +383,6 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
requestAnimationFrame(() => comments.clearFocus())
|
requestAnimationFrame(() => comments.clearFocus())
|
||||||
})
|
})
|
||||||
|
|
||||||
const getCodeScroll = () => {
|
|
||||||
const el = scroll
|
|
||||||
if (!el) return []
|
|
||||||
|
|
||||||
const host = el.querySelector("diffs-container")
|
|
||||||
if (!(host instanceof HTMLElement)) return []
|
|
||||||
|
|
||||||
const root = host.shadowRoot
|
|
||||||
if (!root) return []
|
|
||||||
|
|
||||||
return Array.from(root.querySelectorAll("[data-code]")).filter(
|
|
||||||
(node): node is HTMLElement => node instanceof HTMLElement && node.clientWidth > 0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const queueScrollUpdate = (next: { x: number; y: number }) => {
|
|
||||||
pending = next
|
|
||||||
if (scrollFrame !== undefined) return
|
|
||||||
|
|
||||||
scrollFrame = requestAnimationFrame(() => {
|
|
||||||
scrollFrame = undefined
|
|
||||||
|
|
||||||
const out = pending
|
|
||||||
pending = undefined
|
|
||||||
if (!out) return
|
|
||||||
|
|
||||||
view().setScroll(props.tab, out)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCodeScroll = (event: Event) => {
|
|
||||||
const el = scroll
|
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
const target = event.currentTarget
|
|
||||||
if (!(target instanceof HTMLElement)) return
|
|
||||||
|
|
||||||
queueScrollUpdate({
|
|
||||||
x: target.scrollLeft,
|
|
||||||
y: el.scrollTop,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncCodeScroll = () => {
|
|
||||||
const next = getCodeScroll()
|
|
||||||
if (next.length === codeScroll.length && next.every((el, i) => el === codeScroll[i])) return
|
|
||||||
|
|
||||||
for (const item of codeScroll) {
|
|
||||||
item.removeEventListener("scroll", handleCodeScroll)
|
|
||||||
}
|
|
||||||
|
|
||||||
codeScroll = next
|
|
||||||
|
|
||||||
for (const item of codeScroll) {
|
|
||||||
item.addEventListener("scroll", handleCodeScroll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const restoreScroll = () => {
|
|
||||||
const el = scroll
|
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
const s = view().scroll(props.tab)
|
|
||||||
if (!s) return
|
|
||||||
|
|
||||||
syncCodeScroll()
|
|
||||||
|
|
||||||
if (codeScroll.length > 0) {
|
|
||||||
for (const item of codeScroll) {
|
|
||||||
if (item.scrollLeft !== s.x) item.scrollLeft = s.x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el.scrollTop !== s.y) el.scrollTop = s.y
|
|
||||||
if (codeScroll.length > 0) return
|
|
||||||
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
|
|
||||||
}
|
|
||||||
|
|
||||||
const queueRestore = () => {
|
|
||||||
if (restoreFrame !== undefined) return
|
|
||||||
|
|
||||||
restoreFrame = requestAnimationFrame(() => {
|
|
||||||
restoreFrame = undefined
|
|
||||||
restoreScroll()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
|
|
||||||
if (codeScroll.length === 0) syncCodeScroll()
|
|
||||||
|
|
||||||
queueScrollUpdate({
|
|
||||||
x: codeScroll[0]?.scrollLeft ?? event.currentTarget.scrollLeft,
|
|
||||||
y: event.currentTarget.scrollTop,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelCommenting = () => {
|
const cancelCommenting = () => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (p) file.setSelectedLines(p, null)
|
if (p) file.setSelectedLines(p, null)
|
||||||
@@ -375,16 +402,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active)
|
const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active)
|
||||||
prev = { loaded, ready, active }
|
prev = { loaded, ready, active }
|
||||||
if (!restore) return
|
if (!restore) return
|
||||||
queueRestore()
|
scrollSync.queueRestore()
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
for (const item of codeScroll) {
|
|
||||||
item.removeEventListener("scroll", handleCodeScroll)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
|
|
||||||
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderFile = (source: string) => (
|
const renderFile = (source: string) => (
|
||||||
@@ -402,7 +420,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
selectedLines={activeSelection()}
|
selectedLines={activeSelection()}
|
||||||
commentedLines={commentedLines()}
|
commentedLines={commentedLines()}
|
||||||
onRendered={() => {
|
onRendered={() => {
|
||||||
queueRestore()
|
scrollSync.queueRestore()
|
||||||
}}
|
}}
|
||||||
annotations={commentsUi.annotations()}
|
annotations={commentsUi.annotations()}
|
||||||
renderAnnotation={commentsUi.renderAnnotation}
|
renderAnnotation={commentsUi.renderAnnotation}
|
||||||
@@ -420,7 +438,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
mode: "auto",
|
mode: "auto",
|
||||||
path: path(),
|
path: path(),
|
||||||
current: state()?.content,
|
current: state()?.content,
|
||||||
onLoad: queueRestore,
|
onLoad: scrollSync.queueRestore,
|
||||||
onError: (args: { kind: "image" | "audio" | "svg" }) => {
|
onError: (args: { kind: "image" | "audio" | "svg" }) => {
|
||||||
if (args.kind !== "svg") return
|
if (args.kind !== "svg") return
|
||||||
showToast({
|
showToast({
|
||||||
@@ -435,14 +453,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Content value={props.tab} class="mt-3 relative h-full">
|
<Tabs.Content value={props.tab} class="mt-3 relative h-full">
|
||||||
<ScrollView
|
<ScrollView class="h-full" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll as any}>
|
||||||
class="h-full"
|
|
||||||
viewportRef={(el: HTMLDivElement) => {
|
|
||||||
scroll = el
|
|
||||||
restoreScroll()
|
|
||||||
}}
|
|
||||||
onScroll={handleScroll as any}
|
|
||||||
>
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={state()?.loaded}>{renderFile(contents())}</Match>
|
<Match when={state()?.loaded}>{renderFile(contents())}</Match>
|
||||||
<Match when={state()?.loading}>
|
<Match when={state()?.loading}>
|
||||||
|
|||||||
@@ -128,25 +128,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
|
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
|
||||||
return permission.isAutoAcceptingDirectory(sdk.directory)
|
return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||||
}
|
}
|
||||||
command.register("session", () => {
|
const write = async (value: string) => {
|
||||||
const share =
|
|
||||||
sync.data.config.share === "disabled"
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
sessionCommand({
|
|
||||||
id: "session.share",
|
|
||||||
title: info()?.share?.url
|
|
||||||
? language.t("session.share.copy.copyLink")
|
|
||||||
: language.t("command.session.share"),
|
|
||||||
description: info()?.share?.url
|
|
||||||
? language.t("toast.session.share.success.description")
|
|
||||||
: language.t("command.session.share.description"),
|
|
||||||
slash: "share",
|
|
||||||
disabled: !params.id,
|
|
||||||
onSelect: async () => {
|
|
||||||
if (!params.id) return
|
|
||||||
|
|
||||||
const write = (value: string) => {
|
|
||||||
const body = typeof document === "undefined" ? undefined : document.body
|
const body = typeof document === "undefined" ? undefined : document.body
|
||||||
if (body) {
|
if (body) {
|
||||||
const textarea = document.createElement("textarea")
|
const textarea = document.createElement("textarea")
|
||||||
@@ -159,20 +141,19 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
textarea.select()
|
textarea.select()
|
||||||
const copied = document.execCommand("copy")
|
const copied = document.execCommand("copy")
|
||||||
body.removeChild(textarea)
|
body.removeChild(textarea)
|
||||||
if (copied) return Promise.resolve(true)
|
if (copied) return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
||||||
if (!clipboard?.writeText) return Promise.resolve(false)
|
if (!clipboard?.writeText) return false
|
||||||
return clipboard.writeText(value).then(
|
return clipboard.writeText(value).then(
|
||||||
() => true,
|
() => true,
|
||||||
() => false,
|
() => false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const copy = async (url: string, existing: boolean) => {
|
const copyShare = async (url: string, existing: boolean) => {
|
||||||
const ok = await write(url)
|
if (!(await write(url))) {
|
||||||
if (!ok) {
|
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("toast.session.share.copyFailed.title"),
|
title: language.t("toast.session.share.copyFailed.title"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -181,22 +162,24 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showToast({
|
showToast({
|
||||||
title: existing
|
title: existing ? language.t("session.share.copy.copied") : language.t("toast.session.share.success.title"),
|
||||||
? language.t("session.share.copy.copied")
|
|
||||||
: language.t("toast.session.share.success.title"),
|
|
||||||
description: language.t("toast.session.share.success.description"),
|
description: language.t("toast.session.share.success.description"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const share = async () => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
|
||||||
const existing = info()?.share?.url
|
const existing = info()?.share?.url
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await copy(existing, true)
|
await copyShare(existing, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = await sdk.client.session
|
const url = await sdk.client.session
|
||||||
.share({ sessionID: params.id })
|
.share({ sessionID })
|
||||||
.then((res) => res.data?.share?.url)
|
.then((res) => res.data?.share?.url)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -208,19 +191,15 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await copy(url, false)
|
await copyShare(url, false)
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
sessionCommand({
|
const unshare = async () => {
|
||||||
id: "session.unshare",
|
const sessionID = params.id
|
||||||
title: language.t("command.session.unshare"),
|
if (!sessionID) return
|
||||||
description: language.t("command.session.unshare.description"),
|
|
||||||
slash: "unshare",
|
|
||||||
disabled: !params.id || !info()?.share?.url,
|
|
||||||
onSelect: async () => {
|
|
||||||
if (!params.id) return
|
|
||||||
await sdk.client.session
|
await sdk.client.session
|
||||||
.unshare({ sessionID: params.id })
|
.unshare({ sessionID })
|
||||||
.then(() =>
|
.then(() =>
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("toast.session.unshare.success.title"),
|
title: language.t("toast.session.unshare.success.title"),
|
||||||
@@ -235,50 +214,24 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
variant: "error",
|
variant: "error",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
return [
|
const openFile = () => {
|
||||||
sessionCommand({
|
|
||||||
id: "session.new",
|
|
||||||
title: language.t("command.session.new"),
|
|
||||||
keybind: "mod+shift+s",
|
|
||||||
slash: "new",
|
|
||||||
onSelect: () => navigate(`/${params.dir}/session`),
|
|
||||||
}),
|
|
||||||
fileCommand({
|
|
||||||
id: "file.open",
|
|
||||||
title: language.t("command.file.open"),
|
|
||||||
description: language.t("palette.search.placeholder"),
|
|
||||||
keybind: "mod+k,mod+p",
|
|
||||||
slash: "open",
|
|
||||||
onSelect: () => {
|
|
||||||
void import("@/components/dialog-select-file").then((x) => {
|
void import("@/components/dialog-select-file").then((x) => {
|
||||||
dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />)
|
dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />)
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
fileCommand({
|
const closeTab = () => {
|
||||||
id: "tab.close",
|
|
||||||
title: language.t("command.tab.close"),
|
|
||||||
keybind: "mod+w",
|
|
||||||
disabled: !closableTab(),
|
|
||||||
onSelect: () => {
|
|
||||||
const tab = closableTab()
|
const tab = closableTab()
|
||||||
if (!tab) return
|
if (!tab) return
|
||||||
tabs().close(tab)
|
tabs().close(tab)
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
contextCommand({
|
const addSelection = () => {
|
||||||
id: "context.addSelection",
|
|
||||||
title: language.t("command.context.addSelection"),
|
|
||||||
description: language.t("command.context.addSelection.description"),
|
|
||||||
keybind: "mod+shift+l",
|
|
||||||
disabled: !canAddSelectionContext(),
|
|
||||||
onSelect: () => {
|
|
||||||
const tab = activeFileTab()
|
const tab = activeFileTab()
|
||||||
if (!tab) return
|
if (!tab) return
|
||||||
|
|
||||||
const path = file.pathFromTab(tab)
|
const path = file.pathFromTab(tab)
|
||||||
if (!path) return
|
if (!path) return
|
||||||
|
|
||||||
@@ -292,8 +245,209 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSelectionToContext(path, selectionFromLines(range))
|
addSelectionToContext(path, selectionFromLines(range))
|
||||||
},
|
}
|
||||||
|
|
||||||
|
const openTerminal = () => {
|
||||||
|
if (terminal.all().length > 0) terminal.new()
|
||||||
|
view().terminal.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
const chooseModel = () => {
|
||||||
|
void import("@/components/dialog-select-model").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectModel model={local.model} />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const chooseMcp = () => {
|
||||||
|
void import("@/components/dialog-select-mcp").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectMcp />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleAutoAccept = () => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (sessionID) permission.toggleAutoAccept(sessionID, sdk.directory)
|
||||||
|
else permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||||
|
|
||||||
|
const active = sessionID
|
||||||
|
? permission.isAutoAccepting(sessionID, sdk.directory)
|
||||||
|
: permission.isAutoAcceptingDirectory(sdk.directory)
|
||||||
|
showToast({
|
||||||
|
title: active
|
||||||
|
? language.t("toast.permissions.autoaccept.on.title")
|
||||||
|
: language.t("toast.permissions.autoaccept.off.title"),
|
||||||
|
description: active
|
||||||
|
? language.t("toast.permissions.autoaccept.on.description")
|
||||||
|
: language.t("toast.permissions.autoaccept.off.description"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const undo = async () => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
|
||||||
|
if (status().type !== "idle") {
|
||||||
|
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const revert = info()?.revert?.messageID
|
||||||
|
const message = findLast(userMessages(), (x) => !revert || x.id < revert)
|
||||||
|
if (!message) return
|
||||||
|
|
||||||
|
await sdk.client.session.revert({ sessionID, messageID: message.id })
|
||||||
|
const parts = sync.data.part[message.id]
|
||||||
|
if (parts) {
|
||||||
|
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
||||||
|
prompt.set(restored)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = findLast(userMessages(), (x) => x.id < message.id)
|
||||||
|
setActiveMessage(prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
const redo = async () => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
|
||||||
|
const revertMessageID = info()?.revert?.messageID
|
||||||
|
if (!revertMessageID) return
|
||||||
|
|
||||||
|
const next = userMessages().find((x) => x.id > revertMessageID)
|
||||||
|
if (!next) {
|
||||||
|
await sdk.client.session.unrevert({ sessionID })
|
||||||
|
prompt.reset()
|
||||||
|
const last = findLast(userMessages(), (x) => x.id >= revertMessageID)
|
||||||
|
setActiveMessage(last)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await sdk.client.session.revert({ sessionID, messageID: next.id })
|
||||||
|
const prev = findLast(userMessages(), (x) => x.id < next.id)
|
||||||
|
setActiveMessage(prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
const compact = async () => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
|
||||||
|
const model = local.model.current()
|
||||||
|
if (!model) {
|
||||||
|
showToast({
|
||||||
|
title: language.t("toast.model.none.title"),
|
||||||
|
description: language.t("toast.model.none.description"),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await sdk.client.session.summarize({
|
||||||
|
sessionID,
|
||||||
|
modelID: model.id,
|
||||||
|
providerID: model.provider.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fork = () => {
|
||||||
|
void import("@/components/dialog-fork").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogFork />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareCmds = () => {
|
||||||
|
if (sync.data.config.share === "disabled") return []
|
||||||
|
return [
|
||||||
|
sessionCommand({
|
||||||
|
id: "session.share",
|
||||||
|
title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"),
|
||||||
|
description: info()?.share?.url
|
||||||
|
? language.t("toast.session.share.success.description")
|
||||||
|
: language.t("command.session.share.description"),
|
||||||
|
slash: "share",
|
||||||
|
disabled: !params.id,
|
||||||
|
onSelect: share,
|
||||||
}),
|
}),
|
||||||
|
sessionCommand({
|
||||||
|
id: "session.unshare",
|
||||||
|
title: language.t("command.session.unshare"),
|
||||||
|
description: language.t("command.session.unshare.description"),
|
||||||
|
slash: "unshare",
|
||||||
|
disabled: !params.id || !info()?.share?.url,
|
||||||
|
onSelect: unshare,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionCmds = () => [
|
||||||
|
sessionCommand({
|
||||||
|
id: "session.new",
|
||||||
|
title: language.t("command.session.new"),
|
||||||
|
keybind: "mod+shift+s",
|
||||||
|
slash: "new",
|
||||||
|
onSelect: () => navigate(`/${params.dir}/session`),
|
||||||
|
}),
|
||||||
|
sessionCommand({
|
||||||
|
id: "session.undo",
|
||||||
|
title: language.t("command.session.undo"),
|
||||||
|
description: language.t("command.session.undo.description"),
|
||||||
|
slash: "undo",
|
||||||
|
disabled: !params.id || visibleUserMessages().length === 0,
|
||||||
|
onSelect: undo,
|
||||||
|
}),
|
||||||
|
sessionCommand({
|
||||||
|
id: "session.redo",
|
||||||
|
title: language.t("command.session.redo"),
|
||||||
|
description: language.t("command.session.redo.description"),
|
||||||
|
slash: "redo",
|
||||||
|
disabled: !params.id || !info()?.revert?.messageID,
|
||||||
|
onSelect: redo,
|
||||||
|
}),
|
||||||
|
sessionCommand({
|
||||||
|
id: "session.compact",
|
||||||
|
title: language.t("command.session.compact"),
|
||||||
|
description: language.t("command.session.compact.description"),
|
||||||
|
slash: "compact",
|
||||||
|
disabled: !params.id || visibleUserMessages().length === 0,
|
||||||
|
onSelect: compact,
|
||||||
|
}),
|
||||||
|
sessionCommand({
|
||||||
|
id: "session.fork",
|
||||||
|
title: language.t("command.session.fork"),
|
||||||
|
description: language.t("command.session.fork.description"),
|
||||||
|
slash: "fork",
|
||||||
|
disabled: !params.id || visibleUserMessages().length === 0,
|
||||||
|
onSelect: fork,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const fileCmds = () => [
|
||||||
|
fileCommand({
|
||||||
|
id: "file.open",
|
||||||
|
title: language.t("command.file.open"),
|
||||||
|
description: language.t("palette.search.placeholder"),
|
||||||
|
keybind: "mod+k,mod+p",
|
||||||
|
slash: "open",
|
||||||
|
onSelect: openFile,
|
||||||
|
}),
|
||||||
|
fileCommand({
|
||||||
|
id: "tab.close",
|
||||||
|
title: language.t("command.tab.close"),
|
||||||
|
keybind: "mod+w",
|
||||||
|
disabled: !closableTab(),
|
||||||
|
onSelect: closeTab,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const contextCmds = () => [
|
||||||
|
contextCommand({
|
||||||
|
id: "context.addSelection",
|
||||||
|
title: language.t("command.context.addSelection"),
|
||||||
|
description: language.t("command.context.addSelection.description"),
|
||||||
|
keybind: "mod+shift+l",
|
||||||
|
disabled: !canAddSelectionContext(),
|
||||||
|
onSelect: addSelection,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const viewCmds = () => [
|
||||||
viewCommand({
|
viewCommand({
|
||||||
id: "terminal.toggle",
|
id: "terminal.toggle",
|
||||||
title: language.t("command.terminal.toggle"),
|
title: language.t("command.terminal.toggle"),
|
||||||
@@ -319,16 +473,19 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
keybind: "ctrl+l",
|
keybind: "ctrl+l",
|
||||||
onSelect: focusInput,
|
onSelect: focusInput,
|
||||||
}),
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const terminalCmds = () => [
|
||||||
terminalCommand({
|
terminalCommand({
|
||||||
id: "terminal.new",
|
id: "terminal.new",
|
||||||
title: language.t("command.terminal.new"),
|
title: language.t("command.terminal.new"),
|
||||||
description: language.t("command.terminal.new.description"),
|
description: language.t("command.terminal.new.description"),
|
||||||
keybind: "ctrl+alt+t",
|
keybind: "ctrl+alt+t",
|
||||||
onSelect: () => {
|
onSelect: openTerminal,
|
||||||
if (terminal.all().length > 0) terminal.new()
|
|
||||||
view().terminal.open()
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const messageCmds = () => [
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "message.previous",
|
id: "message.previous",
|
||||||
title: language.t("command.message.previous"),
|
title: language.t("command.message.previous"),
|
||||||
@@ -345,30 +502,38 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
disabled: !params.id,
|
disabled: !params.id,
|
||||||
onSelect: () => navigateMessageByOffset(1),
|
onSelect: () => navigateMessageByOffset(1),
|
||||||
}),
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const modelCmds = () => [
|
||||||
modelCommand({
|
modelCommand({
|
||||||
id: "model.choose",
|
id: "model.choose",
|
||||||
title: language.t("command.model.choose"),
|
title: language.t("command.model.choose"),
|
||||||
description: language.t("command.model.choose.description"),
|
description: language.t("command.model.choose.description"),
|
||||||
keybind: "mod+'",
|
keybind: "mod+'",
|
||||||
slash: "model",
|
slash: "model",
|
||||||
onSelect: () => {
|
onSelect: chooseModel,
|
||||||
void import("@/components/dialog-select-model").then((x) => {
|
|
||||||
dialog.show(() => <x.DialogSelectModel model={local.model} />)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
modelCommand({
|
||||||
|
id: "model.variant.cycle",
|
||||||
|
title: language.t("command.model.variant.cycle"),
|
||||||
|
description: language.t("command.model.variant.cycle.description"),
|
||||||
|
keybind: "shift+mod+d",
|
||||||
|
onSelect: () => local.model.variant.cycle(),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const mcpCmds = () => [
|
||||||
mcpCommand({
|
mcpCommand({
|
||||||
id: "mcp.toggle",
|
id: "mcp.toggle",
|
||||||
title: language.t("command.mcp.toggle"),
|
title: language.t("command.mcp.toggle"),
|
||||||
description: language.t("command.mcp.toggle.description"),
|
description: language.t("command.mcp.toggle.description"),
|
||||||
keybind: "mod+;",
|
keybind: "mod+;",
|
||||||
slash: "mcp",
|
slash: "mcp",
|
||||||
onSelect: () => {
|
onSelect: chooseMcp,
|
||||||
void import("@/components/dialog-select-mcp").then((x) => {
|
|
||||||
dialog.show(() => <x.DialogSelectMcp />)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const agentCmds = () => [
|
||||||
agentCommand({
|
agentCommand({
|
||||||
id: "agent.cycle",
|
id: "agent.cycle",
|
||||||
title: language.t("command.agent.cycle"),
|
title: language.t("command.agent.cycle"),
|
||||||
@@ -384,13 +549,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
keybind: "shift+mod+.",
|
keybind: "shift+mod+.",
|
||||||
onSelect: () => local.agent.move(-1),
|
onSelect: () => local.agent.move(-1),
|
||||||
}),
|
}),
|
||||||
modelCommand({
|
]
|
||||||
id: "model.variant.cycle",
|
|
||||||
title: language.t("command.model.variant.cycle"),
|
const permissionsCmds = () => [
|
||||||
description: language.t("command.model.variant.cycle.description"),
|
|
||||||
keybind: "shift+mod+d",
|
|
||||||
onSelect: () => local.model.variant.cycle(),
|
|
||||||
}),
|
|
||||||
permissionsCommand({
|
permissionsCommand({
|
||||||
id: "permissions.autoaccept",
|
id: "permissions.autoaccept",
|
||||||
title: isAutoAcceptActive()
|
title: isAutoAcceptActive()
|
||||||
@@ -398,110 +559,21 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
: language.t("command.permissions.autoaccept.enable"),
|
: language.t("command.permissions.autoaccept.enable"),
|
||||||
keybind: "mod+shift+a",
|
keybind: "mod+shift+a",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
onSelect: () => {
|
onSelect: toggleAutoAccept,
|
||||||
const sessionID = params.id
|
|
||||||
if (sessionID) permission.toggleAutoAccept(sessionID, sdk.directory)
|
|
||||||
else permission.toggleAutoAcceptDirectory(sdk.directory)
|
|
||||||
|
|
||||||
const active = sessionID
|
|
||||||
? permission.isAutoAccepting(sessionID, sdk.directory)
|
|
||||||
: permission.isAutoAcceptingDirectory(sdk.directory)
|
|
||||||
showToast({
|
|
||||||
title: active
|
|
||||||
? language.t("toast.permissions.autoaccept.on.title")
|
|
||||||
: language.t("toast.permissions.autoaccept.off.title"),
|
|
||||||
description: active
|
|
||||||
? language.t("toast.permissions.autoaccept.on.description")
|
|
||||||
: language.t("toast.permissions.autoaccept.off.description"),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
sessionCommand({
|
|
||||||
id: "session.undo",
|
|
||||||
title: language.t("command.session.undo"),
|
|
||||||
description: language.t("command.session.undo.description"),
|
|
||||||
slash: "undo",
|
|
||||||
disabled: !params.id || visibleUserMessages().length === 0,
|
|
||||||
onSelect: async () => {
|
|
||||||
const sessionID = params.id
|
|
||||||
if (!sessionID) return
|
|
||||||
if (status().type !== "idle") {
|
|
||||||
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
|
||||||
}
|
|
||||||
const revert = info()?.revert?.messageID
|
|
||||||
const message = findLast(userMessages(), (x) => !revert || x.id < revert)
|
|
||||||
if (!message) return
|
|
||||||
await sdk.client.session.revert({ sessionID, messageID: message.id })
|
|
||||||
const parts = sync.data.part[message.id]
|
|
||||||
if (parts) {
|
|
||||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
|
||||||
prompt.set(restored)
|
|
||||||
}
|
|
||||||
const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
|
|
||||||
setActiveMessage(priorMessage)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
sessionCommand({
|
|
||||||
id: "session.redo",
|
|
||||||
title: language.t("command.session.redo"),
|
|
||||||
description: language.t("command.session.redo.description"),
|
|
||||||
slash: "redo",
|
|
||||||
disabled: !params.id || !info()?.revert?.messageID,
|
|
||||||
onSelect: async () => {
|
|
||||||
const sessionID = params.id
|
|
||||||
if (!sessionID) return
|
|
||||||
const revertMessageID = info()?.revert?.messageID
|
|
||||||
if (!revertMessageID) return
|
|
||||||
const nextMessage = userMessages().find((x) => x.id > revertMessageID)
|
|
||||||
if (!nextMessage) {
|
|
||||||
await sdk.client.session.unrevert({ sessionID })
|
|
||||||
prompt.reset()
|
|
||||||
const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID)
|
|
||||||
setActiveMessage(lastMsg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
|
||||||
const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
|
|
||||||
setActiveMessage(priorMsg)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
sessionCommand({
|
|
||||||
id: "session.compact",
|
|
||||||
title: language.t("command.session.compact"),
|
|
||||||
description: language.t("command.session.compact.description"),
|
|
||||||
slash: "compact",
|
|
||||||
disabled: !params.id || visibleUserMessages().length === 0,
|
|
||||||
onSelect: async () => {
|
|
||||||
const sessionID = params.id
|
|
||||||
if (!sessionID) return
|
|
||||||
const model = local.model.current()
|
|
||||||
if (!model) {
|
|
||||||
showToast({
|
|
||||||
title: language.t("toast.model.none.title"),
|
|
||||||
description: language.t("toast.model.none.description"),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await sdk.client.session.summarize({
|
|
||||||
sessionID,
|
|
||||||
modelID: model.id,
|
|
||||||
providerID: model.provider.id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
sessionCommand({
|
|
||||||
id: "session.fork",
|
|
||||||
title: language.t("command.session.fork"),
|
|
||||||
description: language.t("command.session.fork.description"),
|
|
||||||
slash: "fork",
|
|
||||||
disabled: !params.id || visibleUserMessages().length === 0,
|
|
||||||
onSelect: () => {
|
|
||||||
void import("@/components/dialog-fork").then((x) => {
|
|
||||||
dialog.show(() => <x.DialogFork />)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
...share,
|
|
||||||
]
|
]
|
||||||
})
|
|
||||||
|
command.register("session", () => [
|
||||||
|
...sessionCmds(),
|
||||||
|
...shareCmds(),
|
||||||
|
...fileCmds(),
|
||||||
|
...contextCmds(),
|
||||||
|
...viewCmds(),
|
||||||
|
...terminalCmds(),
|
||||||
|
...messageCmds(),
|
||||||
|
...modelCmds(),
|
||||||
|
...mcpCmds(),
|
||||||
|
...agentCmds(),
|
||||||
|
...permissionsCmds(),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ beforeEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("theme preload", () => {
|
describe("theme preload", () => {
|
||||||
test("migrates legacy oc-1 to oc-2 before mount", () => {
|
test("resets unsupported built-in theme ids before mount", () => {
|
||||||
localStorage.setItem("opencode-theme-id", "oc-1")
|
localStorage.setItem("opencode-theme-id", "oc-1")
|
||||||
localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
|
localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
|
||||||
localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;")
|
localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;")
|
||||||
@@ -28,10 +28,9 @@ describe("theme preload", () => {
|
|||||||
|
|
||||||
expect(document.documentElement.dataset.theme).toBe("oc-2")
|
expect(document.documentElement.dataset.theme).toBe("oc-2")
|
||||||
expect(document.documentElement.dataset.colorScheme).toBe("light")
|
expect(document.documentElement.dataset.colorScheme).toBe("light")
|
||||||
expect(localStorage.getItem("opencode-theme-id")).toBe("oc-2")
|
expect(document.getElementById("oc-theme-preload")).toBeNull()
|
||||||
expect(localStorage.getItem("opencode-theme-css-light")).toBeNull()
|
expect(localStorage.getItem("opencode-theme-css-light")).toBeNull()
|
||||||
expect(localStorage.getItem("opencode-theme-css-dark")).toBeNull()
|
expect(localStorage.getItem("opencode-theme-css-dark")).toBeNull()
|
||||||
expect(document.getElementById("oc-theme-preload")).toBeNull()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps cached css for non-default themes", () => {
|
test("keeps cached css for non-default themes", () => {
|
||||||
|
|||||||
@@ -15,11 +15,8 @@ type PersistedWithReady<T> = [
|
|||||||
type PersistTarget = {
|
type PersistTarget = {
|
||||||
storage?: string
|
storage?: string
|
||||||
key: string
|
key: string
|
||||||
legacy?: string[]
|
|
||||||
migrate?: (value: unknown) => unknown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEGACY_STORAGE = "default.dat"
|
|
||||||
const GLOBAL_STORAGE = "opencode.global.dat"
|
const GLOBAL_STORAGE = "opencode.global.dat"
|
||||||
const LOCAL_PREFIX = "opencode."
|
const LOCAL_PREFIX = "opencode."
|
||||||
const fallback = new Map<string, boolean>()
|
const fallback = new Map<string, boolean>()
|
||||||
@@ -200,11 +197,10 @@ function parse(value: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalize(defaults: unknown, raw: string, migrate?: (value: unknown) => unknown) {
|
function normalize(defaults: unknown, raw: string) {
|
||||||
const parsed = parse(raw)
|
const parsed = parse(raw)
|
||||||
if (parsed === undefined) return
|
if (parsed === undefined) return
|
||||||
const migrated = migrate ? migrate(parsed) : parsed
|
const merged = merge(defaults, parsed)
|
||||||
const merged = merge(defaults, migrated)
|
|
||||||
return JSON.stringify(merged)
|
return JSON.stringify(merged)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,18 +305,18 @@ export const PersistTesting = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Persist = {
|
export const Persist = {
|
||||||
global(key: string, legacy?: string[]): PersistTarget {
|
global(key: string): PersistTarget {
|
||||||
return { storage: GLOBAL_STORAGE, key, legacy }
|
return { storage: GLOBAL_STORAGE, key }
|
||||||
},
|
},
|
||||||
workspace(dir: string, key: string, legacy?: string[]): PersistTarget {
|
workspace(dir: string, key: string): PersistTarget {
|
||||||
return { storage: workspaceStorage(dir), key: `workspace:${key}`, legacy }
|
return { storage: workspaceStorage(dir), key: `workspace:${key}` }
|
||||||
},
|
},
|
||||||
session(dir: string, session: string, key: string, legacy?: string[]): PersistTarget {
|
session(dir: string, session: string, key: string): PersistTarget {
|
||||||
return { storage: workspaceStorage(dir), key: `session:${session}:${key}`, legacy }
|
return { storage: workspaceStorage(dir), key: `session:${session}:${key}` }
|
||||||
},
|
},
|
||||||
scoped(dir: string, session: string | undefined, key: string, legacy?: string[]): PersistTarget {
|
scoped(dir: string, session: string | undefined, key: string): PersistTarget {
|
||||||
if (session) return Persist.session(dir, session, key, legacy)
|
if (session) return Persist.session(dir, session, key)
|
||||||
return Persist.workspace(dir, key, legacy)
|
return Persist.workspace(dir, key)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +343,6 @@ export function persisted<T>(
|
|||||||
const config: PersistTarget = typeof target === "string" ? { key: target } : target
|
const config: PersistTarget = typeof target === "string" ? { key: target } : target
|
||||||
|
|
||||||
const defaults = snapshot(store[0])
|
const defaults = snapshot(store[0])
|
||||||
const legacy = config.legacy ?? []
|
|
||||||
|
|
||||||
const isDesktop = platform.platform === "desktop" && !!platform.storage
|
const isDesktop = platform.platform === "desktop" && !!platform.storage
|
||||||
|
|
||||||
@@ -357,22 +352,15 @@ export function persisted<T>(
|
|||||||
return localStorageWithPrefix(config.storage)
|
return localStorageWithPrefix(config.storage)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const legacyStorage = (() => {
|
|
||||||
if (!isDesktop) return localStorageDirect()
|
|
||||||
if (!config.storage) return platform.storage?.()
|
|
||||||
return platform.storage?.(LEGACY_STORAGE)
|
|
||||||
})()
|
|
||||||
|
|
||||||
const storage = (() => {
|
const storage = (() => {
|
||||||
if (!isDesktop) {
|
if (!isDesktop) {
|
||||||
const current = currentStorage as SyncStorage
|
const current = currentStorage as SyncStorage
|
||||||
const legacyStore = legacyStorage as SyncStorage
|
|
||||||
|
|
||||||
const api: SyncStorage = {
|
const api: SyncStorage = {
|
||||||
getItem: (key) => {
|
getItem: (key) => {
|
||||||
const raw = current.getItem(key)
|
const raw = current.getItem(key)
|
||||||
if (raw !== null) {
|
if (raw !== null) {
|
||||||
const next = normalize(defaults, raw, config.migrate)
|
const next = normalize(defaults, raw)
|
||||||
if (next === undefined) {
|
if (next === undefined) {
|
||||||
current.removeItem(key)
|
current.removeItem(key)
|
||||||
return null
|
return null
|
||||||
@@ -381,20 +369,6 @@ export function persisted<T>(
|
|||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const legacyKey of legacy) {
|
|
||||||
const legacyRaw = legacyStore.getItem(legacyKey)
|
|
||||||
if (legacyRaw === null) continue
|
|
||||||
|
|
||||||
const next = normalize(defaults, legacyRaw, config.migrate)
|
|
||||||
if (next === undefined) {
|
|
||||||
legacyStore.removeItem(legacyKey)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
current.setItem(key, next)
|
|
||||||
legacyStore.removeItem(legacyKey)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
setItem: (key, value) => {
|
setItem: (key, value) => {
|
||||||
@@ -409,13 +383,12 @@ export function persisted<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const current = currentStorage as AsyncStorage
|
const current = currentStorage as AsyncStorage
|
||||||
const legacyStore = legacyStorage as AsyncStorage | undefined
|
|
||||||
|
|
||||||
const api: AsyncStorage = {
|
const api: AsyncStorage = {
|
||||||
getItem: async (key) => {
|
getItem: async (key) => {
|
||||||
const raw = await current.getItem(key)
|
const raw = await current.getItem(key)
|
||||||
if (raw !== null) {
|
if (raw !== null) {
|
||||||
const next = normalize(defaults, raw, config.migrate)
|
const next = normalize(defaults, raw)
|
||||||
if (next === undefined) {
|
if (next === undefined) {
|
||||||
await current.removeItem(key).catch(() => undefined)
|
await current.removeItem(key).catch(() => undefined)
|
||||||
return null
|
return null
|
||||||
@@ -424,22 +397,6 @@ export function persisted<T>(
|
|||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!legacyStore) return null
|
|
||||||
|
|
||||||
for (const legacyKey of legacy) {
|
|
||||||
const legacyRaw = await legacyStore.getItem(legacyKey)
|
|
||||||
if (legacyRaw === null) continue
|
|
||||||
|
|
||||||
const next = normalize(defaults, legacyRaw, config.migrate)
|
|
||||||
if (next === undefined) {
|
|
||||||
await legacyStore.removeItem(legacyKey).catch(() => undefined)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
await current.setItem(key, next)
|
|
||||||
await legacyStore.removeItem(legacyKey)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
setItem: async (key, value) => {
|
setItem: async (key, value) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -331,8 +331,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "ألغِ في أي وقت.",
|
"go.faq.a4.p3": "ألغِ في أي وقت.",
|
||||||
"go.faq.q5": "ماذا عن البيانات والخصوصية؟",
|
"go.faq.q5": "ماذا عن البيانات والخصوصية؟",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر.",
|
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. يتبع مزودونا سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج.",
|
||||||
"go.faq.a5.contactAfter": "إذا كان لديك أي أسئلة.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"تتم استضافة نماذج Go في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع",
|
"تتم استضافة نماذج Go في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع",
|
||||||
"go.faq.a5.exceptionsLink": "الاستثناءات التالية",
|
"go.faq.a5.exceptionsLink": "الاستثناءات التالية",
|
||||||
|
|||||||
@@ -338,8 +338,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Cancele a qualquer momento.",
|
"go.faq.a4.p3": "Cancele a qualquer momento.",
|
||||||
"go.faq.q5": "E sobre dados e privacidade?",
|
"go.faq.q5": "E sobre dados e privacidade?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável.",
|
"O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Nossos provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos.",
|
||||||
"go.faq.a5.contactAfter": "se você tiver alguma dúvida.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Os modelos Go são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos, com as",
|
"Os modelos Go são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos, com as",
|
||||||
"go.faq.a5.exceptionsLink": "seguintes exceções",
|
"go.faq.a5.exceptionsLink": "seguintes exceções",
|
||||||
|
|||||||
@@ -336,8 +336,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Annuller til enhver tid.",
|
"go.faq.a4.p3": "Annuller til enhver tid.",
|
||||||
"go.faq.q5": "Hvad med data og privatliv?",
|
"go.faq.q5": "Hvad med data og privatliv?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang.",
|
"Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Vores udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning.",
|
||||||
"go.faq.a5.contactAfter": "hvis du har spørgsmål.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Go-modeller hostes i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning, med de",
|
"Go-modeller hostes i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning, med de",
|
||||||
"go.faq.a5.exceptionsLink": "følgende undtagelser",
|
"go.faq.a5.exceptionsLink": "følgende undtagelser",
|
||||||
|
|||||||
@@ -338,8 +338,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Jederzeit kündbar.",
|
"go.faq.a4.p3": "Jederzeit kündbar.",
|
||||||
"go.faq.q5": "Was ist mit Daten und Privatsphäre?",
|
"go.faq.q5": "Was ist mit Daten und Privatsphäre?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"Der Plan ist primär für internationale Nutzer konzipiert, mit Modellen gehostet in den USA, der EU und Singapur für stabilen globalen Zugang.",
|
"Der Plan ist primär für internationale Nutzer konzipiert, mit Modellen gehostet in den USA, der EU und Singapur für stabilen globalen Zugang. Unsere Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen.",
|
||||||
"go.faq.a5.contactAfter": "wenn du Fragen hast.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Go-Modelle werden in den USA gehostet. Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen, mit den",
|
"Go-Modelle werden in den USA gehostet. Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen, mit den",
|
||||||
"go.faq.a5.exceptionsLink": "folgenden Ausnahmen",
|
"go.faq.a5.exceptionsLink": "folgenden Ausnahmen",
|
||||||
|
|||||||
@@ -331,8 +331,8 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Cancel any time.",
|
"go.faq.a4.p3": "Cancel any time.",
|
||||||
"go.faq.q5": "What about data and privacy?",
|
"go.faq.q5": "What about data and privacy?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access.",
|
"The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Our providers follow a zero-retention policy and do not use your data for model training.",
|
||||||
"go.faq.a5.contactAfter": "if you have any questions.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Go models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the",
|
"Go models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the",
|
||||||
"go.faq.a5.exceptionsLink": "following exceptions",
|
"go.faq.a5.exceptionsLink": "following exceptions",
|
||||||
|
|||||||
@@ -338,8 +338,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Cancela en cualquier momento.",
|
"go.faq.a4.p3": "Cancela en cualquier momento.",
|
||||||
"go.faq.q5": "¿Qué pasa con los datos y la privacidad?",
|
"go.faq.q5": "¿Qué pasa con los datos y la privacidad?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., UE y Singapur para un acceso global estable.",
|
"El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., UE y Singapur para un acceso global estable. Nuestros proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos.",
|
||||||
"go.faq.a5.contactAfter": "si tienes alguna pregunta.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Los modelos de Go están alojados en EE. UU. Los proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos, con las",
|
"Los modelos de Go están alojados en EE. UU. Los proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos, con las",
|
||||||
"go.faq.a5.exceptionsLink": "siguientes excepciones",
|
"go.faq.a5.exceptionsLink": "siguientes excepciones",
|
||||||
|
|||||||
@@ -340,8 +340,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Annulez à tout moment.",
|
"go.faq.a4.p3": "Annulez à tout moment.",
|
||||||
"go.faq.q5": "Et pour les données et la confidentialité ?",
|
"go.faq.q5": "Et pour les données et la confidentialité ?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable.",
|
"Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Nos fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles.",
|
||||||
"go.faq.a5.contactAfter": "si vous avez des questions.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Les modèles Go sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les",
|
"Les modèles Go sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les",
|
||||||
"go.faq.a5.exceptionsLink": "exceptions suivantes",
|
"go.faq.a5.exceptionsLink": "exceptions suivantes",
|
||||||
|
|||||||
@@ -334,8 +334,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Annulla in qualsiasi momento.",
|
"go.faq.a4.p3": "Annulla in qualsiasi momento.",
|
||||||
"go.faq.q5": "E per quanto riguarda dati e privacy?",
|
"go.faq.q5": "E per quanto riguarda dati e privacy?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati negli Stati Uniti, UE e Singapore per un accesso globale stabile.",
|
"Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati negli Stati Uniti, UE e Singapore per un accesso globale stabile. I nostri provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli.",
|
||||||
"go.faq.a5.contactAfter": "se hai domande.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"I modelli Go sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le",
|
"I modelli Go sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le",
|
||||||
"go.faq.a5.exceptionsLink": "seguenti eccezioni",
|
"go.faq.a5.exceptionsLink": "seguenti eccezioni",
|
||||||
|
|||||||
@@ -335,8 +335,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "いつでもキャンセル可能です。",
|
"go.faq.a4.p3": "いつでもキャンセル可能です。",
|
||||||
"go.faq.q5": "データとプライバシーは?",
|
"go.faq.q5": "データとプライバシーは?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"このプランは主に海外ユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。",
|
"このプランは主に海外ユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。プロバイダーはゼロ保持ポリシーに従い、お客様のデータをモデルのトレーニングに使用しません。",
|
||||||
"go.faq.a5.contactAfter": "ご質問がございましたら。",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Goのモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(",
|
"Goのモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(",
|
||||||
"go.faq.a5.exceptionsLink": "以下の例外",
|
"go.faq.a5.exceptionsLink": "以下の例外",
|
||||||
|
|||||||
@@ -331,8 +331,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "언제든지 취소할 수 있습니다.",
|
"go.faq.a4.p3": "언제든지 취소할 수 있습니다.",
|
||||||
"go.faq.q5": "데이터와 프라이버시는 어떤가요?",
|
"go.faq.q5": "데이터와 프라이버시는 어떤가요?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU, 싱가포르에 모델이 호스팅되어 있습니다.",
|
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU, 싱가포르에 모델이 호스팅되어 있습니다. 당사의 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다.",
|
||||||
"go.faq.a5.contactAfter": "질문이 있으시면 언제든지 문의해 주세요.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Go 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,",
|
"Go 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,",
|
||||||
"go.faq.a5.exceptionsLink": "다음 예외",
|
"go.faq.a5.exceptionsLink": "다음 예외",
|
||||||
|
|||||||
@@ -335,8 +335,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Avslutt når som helst.",
|
"go.faq.a4.p3": "Avslutt når som helst.",
|
||||||
"go.faq.q5": "Hva med data og personvern?",
|
"go.faq.q5": "Hva med data og personvern?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang.",
|
"Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Våre leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening.",
|
||||||
"go.faq.a5.contactAfter": "hvis du har spørsmål.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Go-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med",
|
"Go-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med",
|
||||||
"go.faq.a5.exceptionsLink": "følgende unntak",
|
"go.faq.a5.exceptionsLink": "følgende unntak",
|
||||||
|
|||||||
@@ -336,8 +336,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Anuluj w dowolnym momencie.",
|
"go.faq.a4.p3": "Anuluj w dowolnym momencie.",
|
||||||
"go.faq.q5": "A co z danymi i prywatnością?",
|
"go.faq.q5": "A co z danymi i prywatnością?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp.",
|
"Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Nasi dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli.",
|
||||||
"go.faq.a5.contactAfter": "jeśli masz jakiekolwiek pytania.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Modele Go są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli, z",
|
"Modele Go są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli, z",
|
||||||
"go.faq.a5.exceptionsLink": "następującymi wyjątkami",
|
"go.faq.a5.exceptionsLink": "następującymi wyjątkami",
|
||||||
|
|||||||
@@ -340,8 +340,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "Отмена в любое время.",
|
"go.faq.a4.p3": "Отмена в любое время.",
|
||||||
"go.faq.q5": "Как насчет данных и приватности?",
|
"go.faq.q5": "Как насчет данных и приватности?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"План разработан в первую очередь для международных пользователей, с моделями, размещенными в США, ЕС и Сингапуре для стабильного глобального доступа.",
|
"План разработан в первую очередь для международных пользователей, с моделями, размещенными в США, ЕС и Сингапуре для стабильного глобального доступа. Наши провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей.",
|
||||||
"go.faq.a5.contactAfter": "если у вас есть вопросы.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Модели Go размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за",
|
"Модели Go размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за",
|
||||||
"go.faq.a5.exceptionsLink": "следующими исключениями",
|
"go.faq.a5.exceptionsLink": "следующими исключениями",
|
||||||
|
|||||||
@@ -333,8 +333,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "ยกเลิกได้ตลอดเวลา",
|
"go.faq.a4.p3": "ยกเลิกได้ตลอดเวลา",
|
||||||
"go.faq.q5": "แล้วเรื่องข้อมูลและความเป็นส่วนตัวล่ะ?",
|
"go.faq.q5": "แล้วเรื่องข้อมูลและความเป็นส่วนตัวล่ะ?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"แผนนี้ออกแบบมาเพื่อผู้ใช้งานระหว่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงทั่วโลกที่เสถียร",
|
"แผนนี้ออกแบบมาเพื่อผู้ใช้งานระหว่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงทั่วโลกที่เสถียร ผู้ให้บริการของเราปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูลและไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล",
|
||||||
"go.faq.a5.contactAfter": "หากคุณมีคำถามใดๆ",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"โมเดล Go โฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี",
|
"โมเดล Go โฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี",
|
||||||
"go.faq.a5.exceptionsLink": "ข้อยกเว้นดังนี้",
|
"go.faq.a5.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||||
|
|||||||
@@ -339,8 +339,7 @@ export const dict = {
|
|||||||
"go.faq.a4.p3": "yönetebilirsiniz. İstediğiniz zaman iptal edin.",
|
"go.faq.a4.p3": "yönetebilirsiniz. İstediğiniz zaman iptal edin.",
|
||||||
"go.faq.q5": "Veri ve gizlilik ne olacak?",
|
"go.faq.q5": "Veri ve gizlilik ne olacak?",
|
||||||
"go.faq.a5.body":
|
"go.faq.a5.body":
|
||||||
"Bu plan öncelikle uluslararası kullanıcılar için tasarlanmış olup, istikrarlı küresel erişim için modeller ABD, AB ve Singapur'da barındırılmaktadır.",
|
"Bu plan öncelikle uluslararası kullanıcılar için tasarlanmış olup, istikrarlı küresel erişim için modeller ABD, AB ve Singapur'da barındırılmaktadır. Sağlayıcılarımız sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz.",
|
||||||
"go.faq.a5.contactAfter": "herhangi bir sorunuz varsa.",
|
|
||||||
"go.faq.a5.beforeExceptions":
|
"go.faq.a5.beforeExceptions":
|
||||||
"Go modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu",
|
"Go modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu",
|
||||||
"go.faq.a5.exceptionsLink": "aşağıdaki istisnalar",
|
"go.faq.a5.exceptionsLink": "aşağıdaki istisnalar",
|
||||||
|
|||||||
@@ -319,8 +319,8 @@ export const dict = {
|
|||||||
"go.faq.a4.p2.accountLink": "账户",
|
"go.faq.a4.p2.accountLink": "账户",
|
||||||
"go.faq.a4.p3": "中管理订阅。随时取消。",
|
"go.faq.a4.p3": "中管理订阅。随时取消。",
|
||||||
"go.faq.q5": "数据和隐私如何?",
|
"go.faq.q5": "数据和隐私如何?",
|
||||||
"go.faq.a5.body": "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保稳定的全球访问。",
|
"go.faq.a5.body":
|
||||||
"go.faq.a5.contactAfter": "如果您有任何问题。",
|
"该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保稳定的全球访问。我们的提供商遵循零留存政策,不使用您的数据进行模型训练。",
|
||||||
"go.faq.a5.beforeExceptions": "Go 模型托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,",
|
"go.faq.a5.beforeExceptions": "Go 模型托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,",
|
||||||
"go.faq.a5.exceptionsLink": "以下例外情况除外",
|
"go.faq.a5.exceptionsLink": "以下例外情况除外",
|
||||||
"go.faq.q6": "我可以充值余额吗?",
|
"go.faq.q6": "我可以充值余额吗?",
|
||||||
|
|||||||
@@ -319,8 +319,8 @@ export const dict = {
|
|||||||
"go.faq.a4.p2.accountLink": "帳戶",
|
"go.faq.a4.p2.accountLink": "帳戶",
|
||||||
"go.faq.a4.p3": "中管理訂閱。隨時取消。",
|
"go.faq.a4.p3": "中管理訂閱。隨時取消。",
|
||||||
"go.faq.q5": "資料與隱私怎麼辦?",
|
"go.faq.q5": "資料與隱私怎麼辦?",
|
||||||
"go.faq.a5.body": "該方案主要面向國際用戶設計,模型託管在美國、歐盟和新加坡,以確保全球穩定存取。",
|
"go.faq.a5.body":
|
||||||
"go.faq.a5.contactAfter": "如果你有任何問題。",
|
"該方案主要面向國際用戶設計,模型託管在美國、歐盟和新加坡,以確保全球穩定存取。我們的供應商遵循零留存政策,不會將你的資料用於模型訓練。",
|
||||||
"go.faq.a5.beforeExceptions": "Go 模型託管在美國。供應商遵循零留存政策,不會將你的資料用於模型訓練,但有",
|
"go.faq.a5.beforeExceptions": "Go 模型託管在美國。供應商遵循零留存政策,不會將你的資料用於模型訓練,但有",
|
||||||
"go.faq.a5.exceptionsLink": "以下例外",
|
"go.faq.a5.exceptionsLink": "以下例外",
|
||||||
"go.faq.q6": "我可以儲值額度嗎?",
|
"go.faq.q6": "我可以儲值額度嗎?",
|
||||||
|
|||||||
@@ -432,10 +432,7 @@ export default function Home() {
|
|||||||
</Faq>
|
</Faq>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Faq question={i18n.t("go.faq.q5")}>
|
<Faq question={i18n.t("go.faq.q5")}>{i18n.t("go.faq.a5.body")}</Faq>
|
||||||
{i18n.t("go.faq.a5.body")} <a href="mailto:contact@anoma.ly">{i18n.t("common.contactUs")}</a>{" "}
|
|
||||||
{i18n.t("go.faq.a5.contactAfter")}
|
|
||||||
</Faq>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Faq question={i18n.t("go.faq.q6")}>{i18n.t("go.faq.a6")}</Faq>
|
<Faq question={i18n.t("go.faq.q6")}>{i18n.t("go.faq.a6")}</Faq>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Billing } from "../src/billing.js"
|
||||||
|
import { Database, eq } from "../src/drizzle/index.js"
|
||||||
|
import { BillingTable } from "../src/schema/billing.sql.js"
|
||||||
|
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
|
||||||
|
import { microCentsToCents } from "../src/util/price.js"
|
||||||
|
|
||||||
|
// get input from command line
|
||||||
|
const workspaceID = process.argv[2]
|
||||||
|
|
||||||
|
if (!workspaceID) {
|
||||||
|
console.error("Usage: bun freeze-workspace.ts <workspaceID>")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check workspace exists
|
||||||
|
const workspace = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(WorkspaceTable)
|
||||||
|
.where(eq(WorkspaceTable.id, workspaceID))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
if (!workspace) {
|
||||||
|
console.error("Error: Workspace not found")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const billing = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(BillingTable)
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const amountInDollars = microCentsToCents(billing.balance) / 100
|
||||||
|
await Billing.grantCredit(workspaceID, 0 - amountInDollars)
|
||||||
|
|
||||||
|
console.log(`Removed payment of $${amountInDollars.toFixed(2)} from workspace ${workspaceID}`)
|
||||||
@@ -18,8 +18,9 @@ import { ModelTable } from "../src/schema/model.sql.js"
|
|||||||
|
|
||||||
// get input from command line
|
// get input from command line
|
||||||
const identifier = process.argv[2]
|
const identifier = process.argv[2]
|
||||||
|
const verbose = process.argv[process.argv.length - 1] === "-v"
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
console.error("Usage: bun lookup-user.ts <email|workspaceID|apiKey>")
|
console.error("Usage: bun lookup-user.ts <email|workspaceID|apiKey> [-v]")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +224,7 @@ async function printWorkspace(workspaceID: string) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
await printTable("28-Day Usage", (tx) =>
|
await printTable("28-Day Usage", (tx) =>
|
||||||
tx
|
tx
|
||||||
.select({
|
.select({
|
||||||
@@ -267,32 +269,6 @@ async function printWorkspace(workspaceID: string) {
|
|||||||
return mapped
|
return mapped
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
/*
|
|
||||||
await printTable("Usage", (tx) =>
|
|
||||||
tx
|
|
||||||
.select({
|
|
||||||
model: UsageTable.model,
|
|
||||||
provider: UsageTable.provider,
|
|
||||||
inputTokens: UsageTable.inputTokens,
|
|
||||||
outputTokens: UsageTable.outputTokens,
|
|
||||||
reasoningTokens: UsageTable.reasoningTokens,
|
|
||||||
cacheReadTokens: UsageTable.cacheReadTokens,
|
|
||||||
cacheWrite5mTokens: UsageTable.cacheWrite5mTokens,
|
|
||||||
cacheWrite1hTokens: UsageTable.cacheWrite1hTokens,
|
|
||||||
cost: UsageTable.cost,
|
|
||||||
timeCreated: UsageTable.timeCreated,
|
|
||||||
})
|
|
||||||
.from(UsageTable)
|
|
||||||
.where(eq(UsageTable.workspaceID, workspace.id))
|
|
||||||
.orderBy(sql`${UsageTable.timeCreated} DESC`)
|
|
||||||
.limit(10)
|
|
||||||
.then((rows) =>
|
|
||||||
rows.map((row) => ({
|
|
||||||
...row,
|
|
||||||
cost: `$${(row.cost / 100000000).toFixed(2)}`,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
await printTable("Disabled Models", (tx) =>
|
await printTable("Disabled Models", (tx) =>
|
||||||
tx
|
tx
|
||||||
.select({
|
.select({
|
||||||
@@ -309,7 +285,7 @@ async function printWorkspace(workspaceID: string) {
|
|||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
*/
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMicroCents(value: number | null | undefined) {
|
function formatMicroCents(value: number | null | undefined) {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
export function centsToMicroCents(amount: number) {
|
export function centsToMicroCents(amount: number) {
|
||||||
return Math.round(amount * 1000000)
|
return Math.round(amount * 1000000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function microCentsToCents(amount: number) {
|
||||||
|
return Math.round(amount / 1000000)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
|
import { execFile } from "node:child_process"
|
||||||
|
import path from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { promisify } from "node:util"
|
||||||
|
|
||||||
import type { Configuration } from "electron-builder"
|
import type { Configuration } from "electron-builder"
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..")
|
||||||
|
const signScript = path.join(rootDir, "script", "sign-windows.ps1")
|
||||||
|
|
||||||
|
async function signWindows(configuration: { path: string }) {
|
||||||
|
if (process.platform !== "win32") return
|
||||||
|
if (process.env.GITHUB_ACTIONS !== "true") return
|
||||||
|
|
||||||
|
await execFileAsync(
|
||||||
|
"pwsh",
|
||||||
|
["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", signScript, configuration.path],
|
||||||
|
{ cwd: rootDir },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const channel = (() => {
|
const channel = (() => {
|
||||||
const raw = process.env.OPENCODE_CHANNEL
|
const raw = process.env.OPENCODE_CHANNEL
|
||||||
if (raw === "dev" || raw === "beta" || raw === "prod") return raw
|
if (raw === "dev" || raw === "beta" || raw === "prod") return raw
|
||||||
@@ -44,6 +64,9 @@ const getBase = (): Configuration => ({
|
|||||||
},
|
},
|
||||||
win: {
|
win: {
|
||||||
icon: `resources/icons/icon.ico`,
|
icon: `resources/icons/icon.ico`,
|
||||||
|
signtoolOptions: {
|
||||||
|
sign: signWindows,
|
||||||
|
},
|
||||||
target: ["nsis"],
|
target: ["nsis"],
|
||||||
},
|
},
|
||||||
nsis: {
|
nsis: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
|
|||||||
console.log(`Updated package.json version to ${Script.version}`)
|
console.log(`Updated package.json version to ${Script.version}`)
|
||||||
|
|
||||||
const sidecarConfig = getCurrentSidecar()
|
const sidecarConfig = getCurrentSidecar()
|
||||||
|
const artifact = process.env.OPENCODE_CLI_ARTIFACT ?? "opencode-cli"
|
||||||
|
|
||||||
const dir = "resources/opencode-binaries"
|
const dir = "resources/opencode-binaries"
|
||||||
|
|
||||||
await $`mkdir -p ${dir}`
|
await $`mkdir -p ${dir}`
|
||||||
await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
|
await $`gh run download ${process.env.GITHUB_RUN_ID} -n ${artifact}`.cwd(dir)
|
||||||
|
|
||||||
await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
|
await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ export async function copyBinaryToSidecarFolder(source: string) {
|
|||||||
await $`mkdir -p ${dir}`
|
await $`mkdir -p ${dir}`
|
||||||
const dest = windowsify(`${dir}/opencode-cli`)
|
const dest = windowsify(`${dir}/opencode-cli`)
|
||||||
await $`cp ${source} ${dest}`
|
await $`cp ${source} ${dest}`
|
||||||
|
if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") {
|
||||||
|
await $`pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File ../../script/sign-windows.ps1 ${dest}`
|
||||||
|
}
|
||||||
if (process.platform === "darwin") await $`codesign --force --sign - ${dest}`
|
if (process.platform === "darwin") await $`codesign --force --sign - ${dest}`
|
||||||
|
|
||||||
console.log(`Copied ${source} to ${dest}`)
|
console.log(`Copied ${source} to ${dest}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
|
|||||||
console.log(`Updated package.json version to ${Script.version}`)
|
console.log(`Updated package.json version to ${Script.version}`)
|
||||||
|
|
||||||
const sidecarConfig = getCurrentSidecar()
|
const sidecarConfig = getCurrentSidecar()
|
||||||
|
const artifact = process.env.OPENCODE_CLI_ARTIFACT ?? "opencode-cli"
|
||||||
|
|
||||||
const dir = "src-tauri/target/opencode-binaries"
|
const dir = "src-tauri/target/opencode-binaries"
|
||||||
|
|
||||||
await $`mkdir -p ${dir}`
|
await $`mkdir -p ${dir}`
|
||||||
await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
|
await $`gh run download ${process.env.GITHUB_RUN_ID} -n ${artifact}`.cwd(dir)
|
||||||
|
|
||||||
await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
|
await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export async function copyBinaryToSidecarFolder(source: string, target = RUST_TA
|
|||||||
await $`mkdir -p src-tauri/sidecars`
|
await $`mkdir -p src-tauri/sidecars`
|
||||||
const dest = windowsify(`src-tauri/sidecars/opencode-cli-${target}`)
|
const dest = windowsify(`src-tauri/sidecars/opencode-cli-${target}`)
|
||||||
await $`cp ${source} ${dest}`
|
await $`cp ${source} ${dest}`
|
||||||
|
if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") {
|
||||||
|
await $`pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File ../../script/sign-windows.ps1 ${dest}`
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Copied ${source} to ${dest}`)
|
console.log(`Copied ${source} to ${dest}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
"icons/beta/icon.ico"
|
"icons/beta/icon.ico"
|
||||||
],
|
],
|
||||||
"windows": {
|
"windows": {
|
||||||
|
"signCommand": {
|
||||||
|
"cmd": "powershell",
|
||||||
|
"args": ["-ExecutionPolicy", "Bypass", "-File", "../../../script/sign-windows.ps1", "%1"]
|
||||||
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"installerIcon": "icons/beta/icon.ico"
|
"installerIcon": "icons/beta/icon.ico"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
"entitlements": "./entitlements.plist"
|
"entitlements": "./entitlements.plist"
|
||||||
},
|
},
|
||||||
"windows": {
|
"windows": {
|
||||||
|
"signCommand": {
|
||||||
|
"cmd": "powershell",
|
||||||
|
"args": ["-ExecutionPolicy", "Bypass", "-File", "../../../script/sign-windows.ps1", "%1"]
|
||||||
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"installerIcon": "icons/dev/icon.ico",
|
"installerIcon": "icons/dev/icon.ico",
|
||||||
"headerImage": "assets/nsis-header.bmp",
|
"headerImage": "assets/nsis-header.bmp",
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
"icons/prod/icon.ico"
|
"icons/prod/icon.ico"
|
||||||
],
|
],
|
||||||
"windows": {
|
"windows": {
|
||||||
|
"signCommand": {
|
||||||
|
"cmd": "powershell",
|
||||||
|
"args": ["-ExecutionPolicy", "Bypass", "-File", "../../../script/sign-windows.ps1", "%1"]
|
||||||
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"installerIcon": "icons/prod/icon.ico"
|
"installerIcon": "icons/prod/icon.ico"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.3.3"
|
version = "1.3.7"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.3/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.7/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.3/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.7/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.3/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.7/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.3/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.7/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.3/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.7/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
"test": "bun test --timeout 30000",
|
"test": "bun test --timeout 30000",
|
||||||
"build": "bun run script/build.ts",
|
"build": "bun run script/build.ts",
|
||||||
|
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||||
"dev": "bun run --conditions=browser ./src/index.ts",
|
"dev": "bun run --conditions=browser ./src/index.ts",
|
||||||
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
|
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
|
||||||
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
|
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
|
||||||
@@ -101,8 +102,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "2.3.3",
|
"@openrouter/ai-sdk-provider": "2.3.3",
|
||||||
"@opentui/core": "0.1.90",
|
"@opentui/core": "0.1.92",
|
||||||
"@opentui/solid": "0.1.90",
|
"@opentui/solid": "0.1.92",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -132,7 +133,7 @@
|
|||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
"opencode-gitlab-auth": "2.0.0",
|
"opencode-gitlab-auth": "2.0.1",
|
||||||
"opencode-poe-auth": "0.0.1",
|
"opencode-poe-auth": "0.0.1",
|
||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.6",
|
||||||
"partial-json": "0.1.7",
|
"partial-json": "0.1.7",
|
||||||
@@ -141,6 +142,7 @@
|
|||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"tree-sitter-bash": "0.25.0",
|
"tree-sitter-bash": "0.25.0",
|
||||||
|
"tree-sitter-powershell": "0.25.10",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"ulid": "catalog:",
|
"ulid": "catalog:",
|
||||||
"vscode-jsonrpc": "8.2.1",
|
"vscode-jsonrpc": "8.2.1",
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
const raw = process.argv[2]
|
||||||
|
if (!raw) {
|
||||||
|
console.error("Usage: bun run script/upgrade-opentui.ts <version>")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ver = raw.replace(/^v/, "")
|
||||||
|
const root = path.resolve(import.meta.dir, "../../..")
|
||||||
|
const skip = new Set([".git", ".opencode", ".turbo", "dist", "node_modules"])
|
||||||
|
const keys = ["@opentui/core", "@opentui/solid"] as const
|
||||||
|
|
||||||
|
const files = (await Array.fromAsync(new Bun.Glob("**/package.json").scan({ cwd: root }))).filter(
|
||||||
|
(file) => !file.split("/").some((part) => skip.has(part)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const set = (cur: string) => {
|
||||||
|
if (cur.startsWith(">=")) return `>=${ver}`
|
||||||
|
if (cur.startsWith("^")) return `^${ver}`
|
||||||
|
if (cur.startsWith("~")) return `~${ver}`
|
||||||
|
return ver
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = (obj: unknown) => {
|
||||||
|
if (!obj || typeof obj !== "object") return false
|
||||||
|
const map = obj as Record<string, unknown>
|
||||||
|
return keys
|
||||||
|
.map((key) => {
|
||||||
|
const cur = map[key]
|
||||||
|
if (typeof cur !== "string") return false
|
||||||
|
const next = set(cur)
|
||||||
|
if (next === cur) return false
|
||||||
|
map[key] = next
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.some(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = (
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async (rel) => {
|
||||||
|
const file = path.join(root, rel)
|
||||||
|
const txt = await Bun.file(file).text()
|
||||||
|
const json = JSON.parse(txt)
|
||||||
|
const hit = [json.dependencies, json.devDependencies, json.peerDependencies].map(edit).some(Boolean)
|
||||||
|
if (!hit) return null
|
||||||
|
await Bun.write(file, `${JSON.stringify(json, null, 2)}\n`)
|
||||||
|
return rel
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter((item): item is string => item !== null)
|
||||||
|
|
||||||
|
if (out.length === 0) {
|
||||||
|
console.log("No opentui deps found")
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Updated opentui to ${ver} in:`)
|
||||||
|
for (const file of out) {
|
||||||
|
console.log(`- ${file}`)
|
||||||
|
}
|
||||||
@@ -277,6 +277,7 @@ Some services are effectified but still use raw `Filesystem.*` or `Process.spawn
|
|||||||
Similarly, **28 files** still import raw `fs` or `fs/promises` directly. These should migrate to `AppFileSystem` or `Filesystem.*` as they're touched.
|
Similarly, **28 files** still import raw `fs` or `fs/promises` directly. These should migrate to `AppFileSystem` or `Filesystem.*` as they're touched.
|
||||||
|
|
||||||
Current raw fs users that will convert during tool migration:
|
Current raw fs users that will convert during tool migration:
|
||||||
|
|
||||||
- `tool/read.ts` — fs.createReadStream, readline
|
- `tool/read.ts` — fs.createReadStream, readline
|
||||||
- `tool/apply_patch.ts` — fs/promises
|
- `tool/apply_patch.ts` — fs/promises
|
||||||
- `tool/bash.ts` — fs/promises
|
- `tool/bash.ts` — fs/promises
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ Technical reference for the current TUI plugin system.
|
|||||||
- Author package entrypoint is `@opencode-ai/plugin/tui`.
|
- Author package entrypoint is `@opencode-ai/plugin/tui`.
|
||||||
- Internal plugins load inside the CLI app the same way external TUI plugins do.
|
- Internal plugins load inside the CLI app the same way external TUI plugins do.
|
||||||
- Package plugins can be installed from CLI or TUI.
|
- Package plugins can be installed from CLI or TUI.
|
||||||
|
- v1 plugin modules are target-exclusive: a module can export `server` or `tui`, never both.
|
||||||
|
- Server runtime keeps v0 legacy fallback (function exports / enumerated exports) after v1 parsing.
|
||||||
|
|
||||||
## TUI config
|
## TUI config
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ Example:
|
|||||||
- `plugin` entries can be either a string spec or `[spec, options]`.
|
- `plugin` entries can be either a string spec or `[spec, options]`.
|
||||||
- Plugin specs can be npm specs, `file://` URLs, relative paths, or absolute paths.
|
- Plugin specs can be npm specs, `file://` URLs, relative paths, or absolute paths.
|
||||||
- Relative path specs are resolved relative to the config file that declared them.
|
- Relative path specs are resolved relative to the config file that declared them.
|
||||||
|
- A file module listed in `tui.json` must be a TUI module (`default export { id?, tui }`) and must not export `server`.
|
||||||
- Duplicate npm plugins are deduped by package name; higher-precedence config wins.
|
- Duplicate npm plugins are deduped by package name; higher-precedence config wins.
|
||||||
- Duplicate file plugins are deduped by exact resolved file spec. This happens while merging config, before plugin modules are loaded.
|
- Duplicate file plugins are deduped by exact resolved file spec. This happens while merging config, before plugin modules are loaded.
|
||||||
- `plugin_enabled` is keyed by plugin id, not by plugin spec.
|
- `plugin_enabled` is keyed by plugin id, not by plugin spec.
|
||||||
@@ -46,7 +49,7 @@ Minimal module shape:
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import type { TuiPlugin } from "@opencode-ai/plugin/tui"
|
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||||
|
|
||||||
const tui: TuiPlugin = async (api, options, meta) => {
|
const tui: TuiPlugin = async (api, options, meta) => {
|
||||||
api.command.register(() => [
|
api.command.register(() => [
|
||||||
@@ -69,16 +72,20 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const plugin: TuiPluginModule & { id: string } = {
|
||||||
id: "acme.demo",
|
id: "acme.demo",
|
||||||
tui,
|
tui,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
```
|
```
|
||||||
|
|
||||||
- Loader only reads the module default export object. Named exports are ignored.
|
- Loader only reads the module default export object. Named exports are ignored.
|
||||||
- TUI shape is `default export { id?, tui }`.
|
- TUI shape is `default export { id?, tui }`; including `server` is rejected.
|
||||||
|
- A single module cannot export both `server` and `tui`.
|
||||||
- `tui` signature is `(api, options, meta) => Promise<void>`.
|
- `tui` signature is `(api, options, meta) => Promise<void>`.
|
||||||
- If package `exports` contains `./tui`, the loader resolves that entrypoint. Otherwise it uses the resolved package target.
|
- If package `exports` contains `./tui`, the loader resolves that entrypoint. Otherwise it uses the resolved package target.
|
||||||
|
- If a package supports both server and TUI, use separate files and package `exports` (`./server` and `./tui`) so each target resolves to a target-only module.
|
||||||
- File/path plugins must export a non-empty `id`.
|
- File/path plugins must export a non-empty `id`.
|
||||||
- npm plugins may omit `id`; package `name` is used.
|
- npm plugins may omit `id`; package `name` is used.
|
||||||
- Runtime identity is the resolved plugin id. Later plugins with the same id are rejected, including collisions with internal plugin ids.
|
- Runtime identity is the resolved plugin id. Later plugins with the same id are rejected, including collisions with internal plugin ids.
|
||||||
@@ -133,10 +140,13 @@ npm plugins can declare a version compatibility range in `package.json` using th
|
|||||||
- Root-worktree fallback (`worktree === "/"` uses `<directory>/.opencode`) is covered by regression tests.
|
- Root-worktree fallback (`worktree === "/"` uses `<directory>/.opencode`) is covered by regression tests.
|
||||||
- `patchPluginConfig` applies all declared manifest targets (`server` and/or `tui`) in one call.
|
- `patchPluginConfig` applies all declared manifest targets (`server` and/or `tui`) in one call.
|
||||||
- `patchPluginConfig` returns structured result unions (`ok`, `code`, fields by error kind) instead of custom thrown errors.
|
- `patchPluginConfig` returns structured result unions (`ok`, `code`, fields by error kind) instead of custom thrown errors.
|
||||||
|
- `patchPluginConfig` serializes per-target config writes with `Flock.acquire(...)`.
|
||||||
|
- `patchPluginConfig` uses targeted `jsonc-parser` edits, so existing JSONC comments are preserved when plugin entries are added or replaced.
|
||||||
- Without `--force`, an already-configured npm package name is a no-op.
|
- Without `--force`, an already-configured npm package name is a no-op.
|
||||||
- With `--force`, replacement matches by package name. If the existing row is `[spec, options]`, those tuple options are kept.
|
- With `--force`, replacement matches by package name. If the existing row is `[spec, options]`, those tuple options are kept.
|
||||||
- Tuple targets in `oc-plugin` provide default options written into config.
|
- Tuple targets in `oc-plugin` provide default options written into config.
|
||||||
- A package can target `server`, `tui`, or both.
|
- A package can target `server`, `tui`, or both.
|
||||||
|
- If a package targets both, each target must still resolve to a separate target-only module. Do not export `{ server, tui }` from one module.
|
||||||
- There is no uninstall, list, or update CLI command for external plugins.
|
- There is no uninstall, list, or update CLI command for external plugins.
|
||||||
- Local file plugins are configured directly in `tui.json`.
|
- Local file plugins are configured directly in `tui.json`.
|
||||||
|
|
||||||
@@ -156,7 +166,7 @@ Top-level API groups exposed to `tui(api, options, meta)`:
|
|||||||
- `api.app.version`
|
- `api.app.version`
|
||||||
- `api.command.register(cb)` / `api.command.trigger(value)`
|
- `api.command.register(cb)` / `api.command.trigger(value)`
|
||||||
- `api.route.register(routes)` / `api.route.navigate(name, params?)` / `api.route.current`
|
- `api.route.register(routes)` / `api.route.navigate(name, params?)` / `api.route.current`
|
||||||
- `api.ui.Dialog`, `DialogAlert`, `DialogConfirm`, `DialogPrompt`, `DialogSelect`, `ui.toast`, `ui.dialog`
|
- `api.ui.Dialog`, `DialogAlert`, `DialogConfirm`, `DialogPrompt`, `DialogSelect`, `Prompt`, `ui.toast`, `ui.dialog`
|
||||||
- `api.keybind.match`, `print`, `create`
|
- `api.keybind.match`, `print`, `create`
|
||||||
- `api.tuiConfig`
|
- `api.tuiConfig`
|
||||||
- `api.kv.get`, `set`, `ready`
|
- `api.kv.get`, `set`, `ready`
|
||||||
@@ -202,6 +212,7 @@ Command behavior:
|
|||||||
|
|
||||||
- `ui.Dialog` is the base dialog wrapper.
|
- `ui.Dialog` is the base dialog wrapper.
|
||||||
- `ui.DialogAlert`, `ui.DialogConfirm`, `ui.DialogPrompt`, `ui.DialogSelect` are built-in dialog components.
|
- `ui.DialogAlert`, `ui.DialogConfirm`, `ui.DialogPrompt`, `ui.DialogSelect` are built-in dialog components.
|
||||||
|
- `ui.Prompt` renders the same prompt component used by the host app.
|
||||||
- `ui.toast(...)` shows a toast.
|
- `ui.toast(...)` shows a toast.
|
||||||
- `ui.dialog` exposes the host dialog stack:
|
- `ui.dialog` exposes the host dialog stack:
|
||||||
- `replace(render, onClose?)`
|
- `replace(render, onClose?)`
|
||||||
@@ -258,7 +269,9 @@ Theme install behavior:
|
|||||||
|
|
||||||
- Relative theme paths are resolved from the plugin root.
|
- Relative theme paths are resolved from the plugin root.
|
||||||
- Theme name is the JSON basename.
|
- Theme name is the JSON basename.
|
||||||
- Install is skipped if that theme name already exists.
|
- First install writes only when the destination file is missing.
|
||||||
|
- If the theme name already exists, install is skipped unless plugin metadata state is `updated`.
|
||||||
|
- On `updated`, host only rewrites themes previously tracked for that plugin and only when source `mtime`/`size` changed.
|
||||||
- Local plugins persist installed themes under the local `.opencode/themes` area near the plugin config source.
|
- Local plugins persist installed themes under the local `.opencode/themes` area near the plugin config source.
|
||||||
- Global plugins persist installed themes under the global `themes` dir.
|
- Global plugins persist installed themes under the global `themes` dir.
|
||||||
- Invalid or unreadable theme files are ignored.
|
- Invalid or unreadable theme files are ignored.
|
||||||
@@ -269,6 +282,7 @@ Current host slot names:
|
|||||||
|
|
||||||
- `app`
|
- `app`
|
||||||
- `home_logo`
|
- `home_logo`
|
||||||
|
- `home_prompt` with props `{ workspace_id? }`
|
||||||
- `home_bottom`
|
- `home_bottom`
|
||||||
- `sidebar_title` with props `{ session_id, title, share_url? }`
|
- `sidebar_title` with props `{ session_id, title, share_url? }`
|
||||||
- `sidebar_content` with props `{ session_id }`
|
- `sidebar_content` with props `{ session_id }`
|
||||||
@@ -281,7 +295,7 @@ Slot notes:
|
|||||||
- `api.slots.register(plugin)` does not return an unregister function.
|
- `api.slots.register(plugin)` does not return an unregister function.
|
||||||
- Returned ids are `pluginId`, `pluginId:1`, `pluginId:2`, and so on.
|
- Returned ids are `pluginId`, `pluginId:1`, `pluginId:2`, and so on.
|
||||||
- Plugin-provided `id` is not allowed.
|
- Plugin-provided `id` is not allowed.
|
||||||
- The current host renders `home_logo` with `replace`, `sidebar_title` and `sidebar_footer` with `single_winner`, and `app`, `home_bottom`, and `sidebar_content` with the slot library default mode.
|
- The current host renders `home_logo` and `home_prompt` with `replace`, `sidebar_title` and `sidebar_footer` with `single_winner`, and `app`, `home_bottom`, and `sidebar_content` with the slot library default mode.
|
||||||
- Plugins cannot define new slot names in this branch.
|
- Plugins cannot define new slot names in this branch.
|
||||||
|
|
||||||
### Plugin control and lifecycle
|
### Plugin control and lifecycle
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
import type { EventSource } from "./context/sdk"
|
import type { EventSource } from "./context/sdk"
|
||||||
|
import { DialogVariant } from "./component/dialog-variant"
|
||||||
|
|
||||||
function rendererConfig(_config: TuiConfig.Info): CliRendererConfig {
|
function rendererConfig(_config: TuiConfig.Info): CliRendererConfig {
|
||||||
return {
|
return {
|
||||||
@@ -584,11 +585,22 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
value: "variant.cycle",
|
value: "variant.cycle",
|
||||||
keybind: "variant_cycle",
|
keybind: "variant_cycle",
|
||||||
category: "Agent",
|
category: "Agent",
|
||||||
hidden: true,
|
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
local.model.variant.cycle()
|
local.model.variant.cycle()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Switch model variant",
|
||||||
|
value: "variant.list",
|
||||||
|
category: "Agent",
|
||||||
|
hidden: local.model.variant.list().length === 0,
|
||||||
|
slash: {
|
||||||
|
name: "variants",
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.replace(() => <DialogVariant />)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Agent cycle reverse",
|
title: "Agent cycle reverse",
|
||||||
value: "agent.cycle.reverse",
|
value: "agent.cycle.reverse",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
|
|||||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||||
import { useDialog } from "@tui/ui/dialog"
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
||||||
|
import { DialogVariant } from "./dialog-variant"
|
||||||
import { useKeybind } from "../context/keybind"
|
import { useKeybind } from "../context/keybind"
|
||||||
import * as fuzzysort from "fuzzysort"
|
import * as fuzzysort from "fuzzysort"
|
||||||
|
|
||||||
@@ -50,8 +51,7 @@ export function DialogModel(props: { providerID?: string }) {
|
|||||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
dialog.clear()
|
onSelect(provider.id, model.id)
|
||||||
local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true })
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -88,8 +88,7 @@ export function DialogModel(props: { providerID?: string }) {
|
|||||||
disabled: provider.id === "opencode" && model.includes("-nano"),
|
disabled: provider.id === "opencode" && model.includes("-nano"),
|
||||||
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||||
onSelect() {
|
onSelect() {
|
||||||
dialog.clear()
|
onSelect(provider.id, model)
|
||||||
local.model.set({ providerID: provider.id, modelID: model }, { recent: true })
|
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
filter((x) => {
|
filter((x) => {
|
||||||
@@ -135,6 +134,21 @@ export function DialogModel(props: { providerID?: string }) {
|
|||||||
|
|
||||||
const title = createMemo(() => provider()?.name ?? "Select model")
|
const title = createMemo(() => provider()?.name ?? "Select model")
|
||||||
|
|
||||||
|
function onSelect(providerID: string, modelID: string) {
|
||||||
|
local.model.set({ providerID, modelID }, { recent: true })
|
||||||
|
const list = local.model.variant.list()
|
||||||
|
const cur = local.model.variant.selected()
|
||||||
|
if (cur === "default" || (cur && list.includes(cur))) {
|
||||||
|
dialog.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (list.length > 0) {
|
||||||
|
dialog.replace(() => <DialogVariant />)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dialog.clear()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogSelect<ReturnType<typeof options>[number]["value"]>
|
<DialogSelect<ReturnType<typeof options>[number]["value"]>
|
||||||
options={options()}
|
options={options()}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { createMemo } from "solid-js"
|
||||||
|
import { useLocal } from "@tui/context/local"
|
||||||
|
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||||
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
|
|
||||||
|
export function DialogVariant() {
|
||||||
|
const local = useLocal()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const options = createMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: "default",
|
||||||
|
title: "Default",
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.clear()
|
||||||
|
local.model.variant.set(undefined)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...local.model.variant.list().map((variant) => ({
|
||||||
|
value: variant,
|
||||||
|
title: variant,
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.clear()
|
||||||
|
local.model.variant.set(variant)
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogSelect<string>
|
||||||
|
options={options()}
|
||||||
|
title={"Select variant"}
|
||||||
|
current={local.model.variant.selected()}
|
||||||
|
flat={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import path from "path"
|
|||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { useLocal } from "@tui/context/local"
|
import { useLocal } from "@tui/context/local"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import { EmptyBorder } from "@tui/component/border"
|
import { EmptyBorder, SplitBorder } from "@tui/component/border"
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
@@ -22,7 +22,7 @@ import { useKeyboard, useRenderer } from "@opentui/solid"
|
|||||||
import { Editor } from "@tui/util/editor"
|
import { Editor } from "@tui/util/editor"
|
||||||
import { useExit } from "../../context/exit"
|
import { useExit } from "../../context/exit"
|
||||||
import { Clipboard } from "../../util/clipboard"
|
import { Clipboard } from "../../util/clipboard"
|
||||||
import type { FilePart } from "@opencode-ai/sdk/v2"
|
import type { AssistantMessage, FilePart } from "@opencode-ai/sdk/v2"
|
||||||
import { TuiEvent } from "../../event"
|
import { TuiEvent } from "../../event"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
@@ -45,6 +45,10 @@ export type PromptProps = {
|
|||||||
ref?: (ref: PromptRef) => void
|
ref?: (ref: PromptRef) => void
|
||||||
hint?: JSX.Element
|
hint?: JSX.Element
|
||||||
showPlaceholder?: boolean
|
showPlaceholder?: boolean
|
||||||
|
placeholders?: {
|
||||||
|
normal?: string[]
|
||||||
|
shell?: string[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptRef = {
|
export type PromptRef = {
|
||||||
@@ -57,8 +61,15 @@ export type PromptRef = {
|
|||||||
submit(): void
|
submit(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
|
const money = new Intl.NumberFormat("en-US", {
|
||||||
const SHELL_PLACEHOLDERS = ["ls -la", "git status", "pwd"]
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
})
|
||||||
|
|
||||||
|
function randomIndex(count: number) {
|
||||||
|
if (count <= 0) return 0
|
||||||
|
return Math.floor(Math.random() * count)
|
||||||
|
}
|
||||||
|
|
||||||
export function Prompt(props: PromptProps) {
|
export function Prompt(props: PromptProps) {
|
||||||
let input: TextareaRenderable
|
let input: TextareaRenderable
|
||||||
@@ -79,6 +90,8 @@ export function Prompt(props: PromptProps) {
|
|||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
const { theme, syntax } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
const kv = useKV()
|
const kv = useKV()
|
||||||
|
const list = createMemo(() => props.placeholders?.normal ?? [])
|
||||||
|
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
||||||
|
|
||||||
function promptModelWarning() {
|
function promptModelWarning() {
|
||||||
toast.show({
|
toast.show({
|
||||||
@@ -122,6 +135,25 @@ export function Prompt(props: PromptProps) {
|
|||||||
return messages.findLast((m) => m.role === "user")
|
return messages.findLast((m) => m.role === "user")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const usage = createMemo(() => {
|
||||||
|
if (!props.sessionID) return
|
||||||
|
const msg = sync.data.message[props.sessionID] ?? []
|
||||||
|
const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
|
||||||
|
if (!last) return
|
||||||
|
|
||||||
|
const tokens =
|
||||||
|
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
||||||
|
if (tokens <= 0) return
|
||||||
|
|
||||||
|
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
||||||
|
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
|
||||||
|
const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0)
|
||||||
|
return {
|
||||||
|
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
|
||||||
|
cost: cost > 0 ? money.format(cost) : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
prompt: PromptInfo
|
prompt: PromptInfo
|
||||||
mode: "normal" | "shell"
|
mode: "normal" | "shell"
|
||||||
@@ -129,7 +161,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
interrupt: number
|
interrupt: number
|
||||||
placeholder: number
|
placeholder: number
|
||||||
}>({
|
}>({
|
||||||
placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
|
placeholder: randomIndex(list().length),
|
||||||
prompt: {
|
prompt: {
|
||||||
input: "",
|
input: "",
|
||||||
parts: [],
|
parts: [],
|
||||||
@@ -143,7 +175,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
on(
|
on(
|
||||||
() => props.sessionID,
|
() => props.sessionID,
|
||||||
() => {
|
() => {
|
||||||
setStore("placeholder", Math.floor(Math.random() * PLACEHOLDERS.length))
|
setStore("placeholder", randomIndex(list().length))
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
@@ -778,12 +810,14 @@ export function Prompt(props: PromptProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const placeholderText = createMemo(() => {
|
const placeholderText = createMemo(() => {
|
||||||
if (props.sessionID) return undefined
|
if (props.showPlaceholder === false) return undefined
|
||||||
if (store.mode === "shell") {
|
if (store.mode === "shell") {
|
||||||
const example = SHELL_PLACEHOLDERS[store.placeholder % SHELL_PLACEHOLDERS.length]
|
if (!shell().length) return undefined
|
||||||
|
const example = shell()[store.placeholder % shell().length]
|
||||||
return `Run a command... "${example}"`
|
return `Run a command... "${example}"`
|
||||||
}
|
}
|
||||||
return `Ask anything... "${PLACEHOLDERS[store.placeholder % PLACEHOLDERS.length]}"`
|
if (!list().length) return undefined
|
||||||
|
return `Ask anything... "${list()[store.placeholder % list().length]}"`
|
||||||
})
|
})
|
||||||
|
|
||||||
const spinnerDef = createMemo(() => {
|
const spinnerDef = createMemo(() => {
|
||||||
@@ -833,8 +867,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
border={["left"]}
|
border={["left"]}
|
||||||
borderColor={highlight()}
|
borderColor={highlight()}
|
||||||
customBorderChars={{
|
customBorderChars={{
|
||||||
...EmptyBorder,
|
...SplitBorder.customBorderChars,
|
||||||
vertical: "┃",
|
|
||||||
bottomLeft: "╹",
|
bottomLeft: "╹",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -848,6 +881,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder={placeholderText()}
|
placeholder={placeholderText()}
|
||||||
|
placeholderColor={theme.textMuted}
|
||||||
textColor={keybind.leader ? theme.textMuted : theme.text}
|
textColor={keybind.leader ? theme.textMuted : theme.text}
|
||||||
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
||||||
minHeight={1}
|
minHeight={1}
|
||||||
@@ -899,7 +933,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.name === "!" && input.visualCursor.offset === 0) {
|
if (e.name === "!" && input.visualCursor.offset === 0) {
|
||||||
setStore("placeholder", Math.floor(Math.random() * SHELL_PLACEHOLDERS.length))
|
setStore("placeholder", randomIndex(shell().length))
|
||||||
setStore("mode", "shell")
|
setStore("mode", "shell")
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
@@ -1074,7 +1108,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<Show when={status().type !== "idle"} fallback={<text />}>
|
<Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
|
||||||
<box
|
<box
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
gap={1}
|
gap={1}
|
||||||
@@ -1158,14 +1192,20 @@ export function Prompt(props: PromptProps) {
|
|||||||
<box gap={2} flexDirection="row">
|
<box gap={2} flexDirection="row">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={store.mode === "normal"}>
|
<Match when={store.mode === "normal"}>
|
||||||
<Show when={local.model.variant.list().length > 0}>
|
<Switch>
|
||||||
<text fg={theme.text}>
|
<Match when={usage()}>
|
||||||
{keybind.print("variant_cycle")} <span style={{ fg: theme.textMuted }}>variants</span>
|
{(item) => (
|
||||||
|
<text fg={theme.textMuted} wrapMode="none">
|
||||||
|
{[item().context, item().cost].filter(Boolean).join(" · ")}
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
)}
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.text}>
|
||||||
{keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>agents</span>
|
{keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>agents</span>
|
||||||
</text>
|
</text>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.text}>
|
||||||
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -321,12 +321,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
current() {
|
selected() {
|
||||||
const m = currentModel()
|
const m = currentModel()
|
||||||
if (!m) return undefined
|
if (!m) return undefined
|
||||||
const key = `${m.providerID}/${m.modelID}`
|
const key = `${m.providerID}/${m.modelID}`
|
||||||
return modelStore.variant[key]
|
return modelStore.variant[key]
|
||||||
},
|
},
|
||||||
|
current() {
|
||||||
|
const v = this.selected()
|
||||||
|
if (!v) return undefined
|
||||||
|
if (!this.list().includes(v)) return undefined
|
||||||
|
return v
|
||||||
|
},
|
||||||
list() {
|
list() {
|
||||||
const m = currentModel()
|
const m = currentModel()
|
||||||
if (!m) return []
|
if (!m) return []
|
||||||
@@ -339,7 +345,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const m = currentModel()
|
const m = currentModel()
|
||||||
if (!m) return
|
if (!m) return
|
||||||
const key = `${m.providerID}/${m.modelID}`
|
const key = `${m.providerID}/${m.modelID}`
|
||||||
setModelStore("variant", key, value)
|
setModelStore("variant", key, value ?? "default")
|
||||||
save()
|
save()
|
||||||
},
|
},
|
||||||
cycle() {
|
cycle() {
|
||||||
|
|||||||
@@ -183,6 +183,18 @@ export function addTheme(name: string, theme: unknown) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function upsertTheme(name: string, theme: unknown) {
|
||||||
|
if (!name) return false
|
||||||
|
if (!isTheme(theme)) return false
|
||||||
|
if (customThemes[name] !== undefined) {
|
||||||
|
customThemes[name] = theme
|
||||||
|
} else {
|
||||||
|
pluginThemes[name] = theme
|
||||||
|
}
|
||||||
|
syncThemes()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||||
const defs = theme.defs ?? {}
|
const defs = theme.defs ?? {}
|
||||||
function resolveColor(c: ColorValue, chain: string[] = []): RGBA {
|
function resolveColor(c: ColorValue, chain: string[] = []): RGBA {
|
||||||
@@ -399,7 +411,16 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const values = createMemo(() => {
|
const values = createMemo(() => {
|
||||||
return resolveTheme(store.themes[store.active] ?? store.themes.opencode, store.mode)
|
const active = store.themes[store.active]
|
||||||
|
if (active) return resolveTheme(active, store.mode)
|
||||||
|
|
||||||
|
const saved = kv.get("theme")
|
||||||
|
if (typeof saved === "string") {
|
||||||
|
const theme = store.themes[saved]
|
||||||
|
if (theme) return resolveTheme(theme, store.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveTheme(store.themes.opencode, store.mode)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||||
|
import { createMemo, Match, Show, Switch } from "solid-js"
|
||||||
|
import { Global } from "@/global"
|
||||||
|
|
||||||
|
const id = "internal:home-footer"
|
||||||
|
|
||||||
|
function Directory(props: { api: TuiPluginApi }) {
|
||||||
|
const theme = () => props.api.theme.current
|
||||||
|
const dir = createMemo(() => {
|
||||||
|
const dir = props.api.state.path.directory || process.cwd()
|
||||||
|
const out = dir.replace(Global.Path.home, "~")
|
||||||
|
const branch = props.api.state.vcs?.branch
|
||||||
|
if (branch) return out + ":" + branch
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
return <text fg={theme().textMuted}>{dir()}</text>
|
||||||
|
}
|
||||||
|
|
||||||
|
function Mcp(props: { api: TuiPluginApi }) {
|
||||||
|
const theme = () => props.api.theme.current
|
||||||
|
const list = createMemo(() => props.api.state.mcp())
|
||||||
|
const has = createMemo(() => list().length > 0)
|
||||||
|
const err = createMemo(() => list().some((item) => item.status === "failed"))
|
||||||
|
const count = createMemo(() => list().filter((item) => item.status === "connected").length)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={has()}>
|
||||||
|
<box gap={1} flexDirection="row" flexShrink={0}>
|
||||||
|
<text fg={theme().text}>
|
||||||
|
<Switch>
|
||||||
|
<Match when={err()}>
|
||||||
|
<span style={{ fg: theme().error }}>⊙ </span>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<span style={{ fg: count() > 0 ? theme().success : theme().textMuted }}>⊙ </span>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
{count()} MCP
|
||||||
|
</text>
|
||||||
|
<text fg={theme().textMuted}>/status</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Version(props: { api: TuiPluginApi }) {
|
||||||
|
const theme = () => props.api.theme.current
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexShrink={0}>
|
||||||
|
<text fg={theme().textMuted}>{props.api.app.version}</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function View(props: { api: TuiPluginApi }) {
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
width="100%"
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
flexDirection="row"
|
||||||
|
flexShrink={0}
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
|
<Directory api={props.api} />
|
||||||
|
<Mcp api={props.api} />
|
||||||
|
<box flexGrow={1} />
|
||||||
|
<Version api={props.api} />
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tui: TuiPlugin = async (api) => {
|
||||||
|
api.slots.register({
|
||||||
|
order: 100,
|
||||||
|
slots: {
|
||||||
|
home_footer() {
|
||||||
|
return <View api={api} />
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: TuiPluginModule & { id: string } = {
|
||||||
|
id,
|
||||||
|
tui,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TuiPlugin } from "@opencode-ai/plugin/tui"
|
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||||
import { createMemo, Show } from "solid-js"
|
import { createMemo, Show } from "solid-js"
|
||||||
import { Tips } from "./tips-view"
|
import { Tips } from "./tips-view"
|
||||||
|
|
||||||
@@ -42,7 +42,9 @@ const tui: TuiPlugin = async (api) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const plugin: TuiPluginModule & { id: string } = {
|
||||||
id,
|
id,
|
||||||
tui,
|
tui,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
|
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
|
||||||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
|
|
||||||
const id = "internal:sidebar-context"
|
const id = "internal:sidebar-context"
|
||||||
@@ -55,7 +55,9 @@ const tui: TuiPlugin = async (api) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const plugin: TuiPluginModule & { id: string } = {
|
||||||
id,
|
id,
|
||||||
tui,
|
tui,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||||
import { createMemo, For, Show, createSignal } from "solid-js"
|
import { createMemo, For, Show, createSignal } from "solid-js"
|
||||||
|
|
||||||
const id = "internal:sidebar-files"
|
const id = "internal:sidebar-files"
|
||||||
@@ -54,7 +54,9 @@ const tui: TuiPlugin = async (api) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const plugin: TuiPluginModule & { id: string } = {
|
||||||
id,
|
id,
|
||||||
tui,
|
tui,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||||
import { createMemo, Show } from "solid-js"
|
import { createMemo, Show } from "solid-js"
|
||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
|
|
||||||
@@ -85,7 +85,9 @@ const tui: TuiPlugin = async (api) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const plugin: TuiPluginModule & { id: string } = {
|
||||||
id,
|
id,
|
||||||
tui,
|
tui,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user