mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-10 19:04:17 +00:00
Compare commits
129 Commits
feature/se
...
sqlite2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4666daa581 | ||
|
|
caf1316116 | ||
|
|
1e2f664410 | ||
|
|
a3aad9c9bf | ||
|
|
eb2587844b | ||
|
|
d863a9cf4e | ||
|
|
7d5be1556a | ||
|
|
659f15aa9b | ||
|
|
d1f5b9e911 | ||
|
|
284b00ff23 | ||
|
|
2c5760742b | ||
|
|
70c794e913 | ||
|
|
3929f0b5bd | ||
|
|
6f5dfe125a | ||
|
|
27fa9dc843 | ||
|
|
1e03a55acd | ||
|
|
65c9669283 | ||
|
|
18b6257119 | ||
|
|
c607c01fb9 | ||
|
|
4c4e30cd71 | ||
|
|
19ad7ad809 | ||
|
|
87795384de | ||
|
|
0732ab3393 | ||
|
|
2bccfd7462 | ||
|
|
83853cc5e6 | ||
|
|
4a73d51acd | ||
|
|
63cd763418 | ||
|
|
32394b699e | ||
|
|
12262862cd | ||
|
|
56a752092e | ||
|
|
439e7ec1fd | ||
|
|
20cf3fc679 | ||
|
|
949f61075f | ||
|
|
705200e199 | ||
|
|
85fa8abd50 | ||
|
|
3118cab2d8 | ||
|
|
31f893f8cb | ||
|
|
056d0c1197 | ||
|
|
1de66812bf | ||
|
|
832902c8e3 | ||
|
|
3d6fb29f0c | ||
|
|
9824370f82 | ||
|
|
371e106faa | ||
|
|
19809e7680 | ||
|
|
389afef336 | ||
|
|
274bb948e7 | ||
|
|
d9b4535d64 | ||
|
|
3dc720ff9c | ||
|
|
56b340b5d5 | ||
|
|
ba740eaefd | ||
|
|
39c5da4405 | ||
|
|
83708c295c | ||
|
|
a84bdd7cd7 | ||
|
|
110f6804fb | ||
|
|
e5ec2f9991 | ||
|
|
7bca3fbf18 | ||
|
|
d578f80f00 | ||
|
|
dc53086c1e | ||
|
|
f74c0339cc | ||
|
|
fb94b4f8e8 | ||
|
|
8ad4768ecd | ||
|
|
24fd8c166d | ||
|
|
a7c5d5ac4c | ||
|
|
5be1202eea | ||
|
|
94d0c9940a | ||
|
|
5952891b1e | ||
|
|
d7c8a3f50d | ||
|
|
ce353819e8 | ||
|
|
2dae94e5a3 | ||
|
|
c6adc19e41 | ||
|
|
ce56166510 | ||
|
|
5911e4c06a | ||
|
|
42fb840f22 | ||
|
|
4dcfdf6572 | ||
|
|
25f3d6d5a9 | ||
|
|
e19a9e9614 | ||
|
|
fcc903489b | ||
|
|
949e69a9bf | ||
|
|
8c30f551e2 | ||
|
|
cb721497c1 | ||
|
|
4ec6293054 | ||
|
|
b7a323355c | ||
|
|
d4f053042c | ||
|
|
5f552534c7 | ||
|
|
ad5b790bb3 | ||
|
|
ed87341c4f | ||
|
|
794ecab028 | ||
|
|
eeb235724b | ||
|
|
61084e7f6f | ||
|
|
200aef2eb3 | ||
|
|
f6e375a555 | ||
|
|
db908deee5 | ||
|
|
7b72cc3a48 | ||
|
|
b8cbfd48ec | ||
|
|
498cbb2c26 | ||
|
|
d6fbd255b6 | ||
|
|
2de1c82bf7 | ||
|
|
34ebb3d051 | ||
|
|
9c3e3c1ab5 | ||
|
|
3ea499f04e | ||
|
|
ab13c1d1c4 | ||
|
|
53b610c331 | ||
|
|
e3519356f2 | ||
|
|
2619acc0ff | ||
|
|
1bc45dc266 | ||
|
|
2e8feb1c78 | ||
|
|
00e60899cc | ||
|
|
30a918e9d4 | ||
|
|
ac16068140 | ||
|
|
19a41ab297 | ||
|
|
cd174d8cba | ||
|
|
246e901e42 | ||
|
|
0ccef1b31f | ||
|
|
7706f5b6a8 | ||
|
|
63e38555c9 | ||
|
|
f40685ab13 | ||
|
|
a48a5a3462 | ||
|
|
5e1639de2b | ||
|
|
2b05833c32 | ||
|
|
acdcf7fa88 | ||
|
|
bf0754caeb | ||
|
|
4d50a32979 | ||
|
|
57edb0ddc5 | ||
|
|
a614b78c6d | ||
|
|
b9f5a34247 | ||
|
|
81b47a44e2 | ||
|
|
0c1c07467e | ||
|
|
105688bf90 | ||
|
|
1e7b4768b1 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💬 Discord Community
|
||||
url: https://discord.gg/opencode
|
||||
|
||||
18
.github/VOUCHED.td
vendored
Normal file
18
.github/VOUCHED.td
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Vouched contributors for this project.
|
||||
#
|
||||
# See https://github.com/mitchellh/vouch for details.
|
||||
#
|
||||
# Syntax:
|
||||
# - One handle per line (without @), sorted alphabetically.
|
||||
# - Optional platform prefix: platform:username (e.g., github:user).
|
||||
# - Denounce with minus prefix: -username or -platform:username.
|
||||
# - Optional details after a space following the handle.
|
||||
adamdotdevin
|
||||
fwang
|
||||
iamdavidhill
|
||||
jayair
|
||||
kitlangton
|
||||
kommander
|
||||
r44vc0rp
|
||||
rekram1-node
|
||||
thdxr
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -1,6 +1,6 @@
|
||||
### What does this PR do?
|
||||
|
||||
Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the pr.
|
||||
Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.
|
||||
|
||||
**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!**
|
||||
|
||||
|
||||
86
.github/workflows/compliance-close.yml
vendored
Normal file
86
.github/workflows/compliance-close.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: compliance-close
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 30 minutes to check for expired compliance windows
|
||||
- cron: "*/30 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
close-non-compliant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close non-compliant issues and PRs after 2 hours
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: items } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: 'needs:compliance',
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
core.info('No open issues/PRs with needs:compliance label');
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const twoHours = 2 * 60 * 60 * 1000;
|
||||
|
||||
for (const item of items) {
|
||||
const isPR = !!item.pull_request;
|
||||
const kind = isPR ? 'PR' : 'issue';
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: item.number,
|
||||
});
|
||||
|
||||
const complianceComment = comments.find(c => c.body.includes('<!-- issue-compliance -->'));
|
||||
if (!complianceComment) continue;
|
||||
|
||||
const commentAge = now - new Date(complianceComment.created_at).getTime();
|
||||
if (commentAge < twoHours) {
|
||||
core.info(`${kind} #${item.number} still within 2-hour window (${Math.round(commentAge / 60000)}m elapsed)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const closeMessage = isPR
|
||||
? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new pull request that follows our guidelines.'
|
||||
: 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new issue that follows our issue templates.';
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: item.number,
|
||||
body: closeMessage,
|
||||
});
|
||||
|
||||
if (isPR) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: item.number,
|
||||
state: 'closed',
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: item.number,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned',
|
||||
});
|
||||
}
|
||||
|
||||
core.info(`Closed non-compliant ${kind} #${item.number} after 2-hour window`);
|
||||
}
|
||||
8
.github/workflows/daily-issues-recap.yml
vendored
8
.github/workflows/daily-issues-recap.yml
vendored
@@ -48,8 +48,12 @@ jobs:
|
||||
TODAY'S DATE: ${TODAY}
|
||||
|
||||
STEP 1: Gather today's issues
|
||||
Search for all issues created today (${TODAY}) using:
|
||||
gh issue list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
|
||||
Search for all OPEN issues created today (${TODAY}) using:
|
||||
gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
|
||||
|
||||
IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these:
|
||||
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
||||
This recap is specifically for COMMUNITY (external) issues only.
|
||||
|
||||
STEP 2: Analyze and categorize
|
||||
For each issue created today, categorize it:
|
||||
|
||||
12
.github/workflows/daily-pr-recap.yml
vendored
12
.github/workflows/daily-pr-recap.yml
vendored
@@ -47,14 +47,18 @@ jobs:
|
||||
TODAY'S DATE: ${TODAY}
|
||||
|
||||
STEP 1: Gather PR data
|
||||
Run these commands to gather PR information. ONLY include PRs created or updated TODAY (${TODAY}):
|
||||
Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}):
|
||||
|
||||
# PRs created today
|
||||
gh pr list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||
# Open PRs created today
|
||||
gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||
|
||||
# PRs with activity today (updated today)
|
||||
# Open PRs with activity today (updated today)
|
||||
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||
|
||||
IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these:
|
||||
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
||||
This recap is specifically for COMMUNITY (external) contributions only.
|
||||
|
||||
|
||||
|
||||
STEP 2: For high-activity PRs, check comment counts
|
||||
|
||||
82
.github/workflows/docs-locale-sync.yml
vendored
Normal file
82
.github/workflows/docs-locale-sync.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: docs-locale-sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- packages/web/src/content/docs/*.mdx
|
||||
|
||||
jobs:
|
||||
sync-locales:
|
||||
if: github.actor != 'opencode-agent[bot]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Compute changed English docs
|
||||
id: changes
|
||||
run: |
|
||||
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'packages/web/src/content/docs/*.mdx' || true)
|
||||
if [ -z "$FILES" ]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No English docs changed in push range"
|
||||
exit 0
|
||||
fi
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "files<<EOF"
|
||||
echo "$FILES"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sync locale docs with OpenCode
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
with:
|
||||
model: opencode/gpt-5.2
|
||||
agent: docs
|
||||
prompt: |
|
||||
Update localized docs to match the latest English docs changes.
|
||||
|
||||
Changed English doc files:
|
||||
<changed_english_docs>
|
||||
${{ steps.changes.outputs.files }}
|
||||
</changed_english_docs>
|
||||
|
||||
Requirements:
|
||||
1. Update all relevant locale docs under packages/web/src/content/docs/<locale>/ so they reflect these English page changes.
|
||||
2. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update.
|
||||
3. Keep locale docs structure aligned with their corresponding English pages.
|
||||
4. Do not modify English source docs in packages/web/src/content/docs/*.mdx.
|
||||
5. If no locale updates are needed, make no changes.
|
||||
|
||||
- name: Commit and push locale docs updates
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "No locale docs changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git add -A
|
||||
git commit -m "docs(i18n): sync locale docs from english changes"
|
||||
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
|
||||
git push origin HEAD:"$GITHUB_REF_NAME"
|
||||
82
.github/workflows/duplicate-issues.yml
vendored
82
.github/workflows/duplicate-issues.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Check for duplicate issues
|
||||
- name: Check duplicates and compliance
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -34,30 +34,84 @@ jobs:
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:'
|
||||
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:
|
||||
|
||||
Issue number:
|
||||
${{ github.event.issue.number }}
|
||||
Issue number: ${{ github.event.issue.number }}
|
||||
|
||||
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
|
||||
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
||||
|
||||
You have TWO tasks. Perform both, then post a SINGLE comment (if needed).
|
||||
|
||||
---
|
||||
|
||||
TASK 1: CONTRIBUTING GUIDELINES COMPLIANCE CHECK
|
||||
|
||||
Check whether the issue follows our contributing guidelines and issue templates.
|
||||
|
||||
This project has three issue templates that every issue MUST use one of:
|
||||
|
||||
1. Bug Report - requires a Description field with real content
|
||||
2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]:
|
||||
3. Question - requires the Question field with real content
|
||||
|
||||
Additionally check:
|
||||
- No AI-generated walls of text (long, AI-generated descriptions are not acceptable)
|
||||
- The issue has real content, not just template placeholder text left unchanged
|
||||
- Bug reports should include some context about how to reproduce
|
||||
- Feature requests should explain the problem or need
|
||||
- We want to push for having the user provide system description & information
|
||||
|
||||
Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content.
|
||||
|
||||
---
|
||||
|
||||
TASK 2: DUPLICATE CHECK
|
||||
|
||||
Search through existing issues (excluding #${{ github.event.issue.number }}) to find potential duplicates.
|
||||
Consider:
|
||||
1. Similar titles or descriptions
|
||||
2. Same error messages or symptoms
|
||||
3. Related functionality or components
|
||||
4. Similar feature requests
|
||||
|
||||
If you find any potential duplicates, please comment on the new issue with:
|
||||
- A brief explanation of why it might be a duplicate
|
||||
- Links to the potentially duplicate issues
|
||||
- A suggestion to check those issues first
|
||||
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, note the pinned keybinds issue #4997.
|
||||
|
||||
---
|
||||
|
||||
POSTING YOUR COMMENT:
|
||||
|
||||
Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows:
|
||||
|
||||
If the issue is NOT compliant, start the comment with:
|
||||
<!-- issue-compliance -->
|
||||
Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
|
||||
|
||||
If duplicates were found, include a section about potential duplicates with links.
|
||||
|
||||
If the issue mentions keybinds/keyboard shortcuts, include a note about #4997.
|
||||
|
||||
If the issue IS compliant AND no duplicates were found AND no keybind reference, do NOT comment at all.
|
||||
|
||||
Use this format for the comment:
|
||||
'This issue might be a duplicate of existing issues. Please check:
|
||||
|
||||
[If not compliant:]
|
||||
<!-- issue-compliance -->
|
||||
This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md).
|
||||
|
||||
**What needs to be fixed:**
|
||||
- [specific reasons]
|
||||
|
||||
Please edit this issue to address the above within **2 hours**, or it will be automatically closed.
|
||||
|
||||
[If duplicates found, add:]
|
||||
---
|
||||
This issue might be a duplicate of existing issues. Please check:
|
||||
- #[issue_number]: [brief description of similarity]
|
||||
|
||||
Feel free to ignore if none of these address your specific case.'
|
||||
[If keybind-related, add:]
|
||||
For keybind-related issues, please also check our pinned keybinds documentation: #4997
|
||||
|
||||
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
|
||||
'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
|
||||
[End with if not compliant:]
|
||||
If you believe this was flagged incorrectly, please let a maintainer know.
|
||||
|
||||
If no clear duplicates are found, do not comment."
|
||||
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
|
||||
|
||||
3
.github/workflows/nix-hashes.yml
vendored
3
.github/workflows/nix-hashes.yml
vendored
@@ -12,6 +12,9 @@ on:
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- "flake.lock"
|
||||
- "nix/node_modules.nix"
|
||||
- "nix/scripts/**"
|
||||
- "patches/**"
|
||||
- ".github/workflows/nix-hashes.yml"
|
||||
|
||||
jobs:
|
||||
|
||||
96
.github/workflows/vouch-check-issue.yml
vendored
Normal file
96
.github/workflows/vouch-check-issue.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: vouch-check-issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if issue author is denounced
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const author = context.payload.issue.user.login;
|
||||
const issueNumber = context.payload.issue.number;
|
||||
|
||||
// Skip bots
|
||||
if (author.endsWith('[bot]')) {
|
||||
core.info(`Skipping bot: ${author}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the VOUCHED.td file via API (no checkout needed)
|
||||
let content;
|
||||
try {
|
||||
const response = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: '.github/VOUCHED.td',
|
||||
});
|
||||
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info('No .github/VOUCHED.td file found, skipping check.');
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Parse the .td file for denounced users
|
||||
const denounced = new Map();
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
if (!trimmed.startsWith('-')) continue;
|
||||
|
||||
const rest = trimmed.slice(1).trim();
|
||||
if (!rest) continue;
|
||||
const spaceIdx = rest.indexOf(' ');
|
||||
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
||||
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
||||
|
||||
// Handle platform:username or bare username
|
||||
// Only match bare usernames or github: prefix (skip other platforms)
|
||||
const colonIdx = handle.indexOf(':');
|
||||
if (colonIdx !== -1) {
|
||||
const platform = handle.slice(0, colonIdx).toLowerCase();
|
||||
if (platform !== 'github') continue;
|
||||
}
|
||||
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
||||
if (!username) continue;
|
||||
|
||||
denounced.set(username.toLowerCase(), reason);
|
||||
}
|
||||
|
||||
// Check if the author is denounced
|
||||
const reason = denounced.get(author.toLowerCase());
|
||||
if (reason === undefined) {
|
||||
core.info(`User ${author} is not denounced. Allowing issue.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Author is denounced — close the issue
|
||||
const body = 'This issue has been automatically closed.';
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned',
|
||||
});
|
||||
|
||||
core.info(`Closed issue #${issueNumber} from denounced user ${author}`);
|
||||
93
.github/workflows/vouch-check-pr.yml
vendored
Normal file
93
.github/workflows/vouch-check-pr.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
name: vouch-check-pr
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if PR author is denounced
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const author = context.payload.pull_request.user.login;
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
|
||||
// Skip bots
|
||||
if (author.endsWith('[bot]')) {
|
||||
core.info(`Skipping bot: ${author}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the VOUCHED.td file via API (no checkout needed)
|
||||
let content;
|
||||
try {
|
||||
const response = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: '.github/VOUCHED.td',
|
||||
});
|
||||
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info('No .github/VOUCHED.td file found, skipping check.');
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Parse the .td file for denounced users
|
||||
const denounced = new Map();
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
if (!trimmed.startsWith('-')) continue;
|
||||
|
||||
const rest = trimmed.slice(1).trim();
|
||||
if (!rest) continue;
|
||||
const spaceIdx = rest.indexOf(' ');
|
||||
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
||||
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
||||
|
||||
// Handle platform:username or bare username
|
||||
// Only match bare usernames or github: prefix (skip other platforms)
|
||||
const colonIdx = handle.indexOf(':');
|
||||
if (colonIdx !== -1) {
|
||||
const platform = handle.slice(0, colonIdx).toLowerCase();
|
||||
if (platform !== 'github') continue;
|
||||
}
|
||||
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
||||
if (!username) continue;
|
||||
|
||||
denounced.set(username.toLowerCase(), reason);
|
||||
}
|
||||
|
||||
// Check if the author is denounced
|
||||
const reason = denounced.get(author.toLowerCase());
|
||||
if (reason === undefined) {
|
||||
core.info(`User ${author} is not denounced. Allowing PR.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Author is denounced — close the PR
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: 'This pull request has been automatically closed.',
|
||||
});
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
state: 'closed',
|
||||
});
|
||||
|
||||
core.info(`Closed PR #${prNumber} from denounced user ${author}`);
|
||||
37
.github/workflows/vouch-manage-by-issue.yml
vendored
Normal file
37
.github/workflows/vouch-manage-by-issue.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: vouch-manage-by-issue
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
concurrency:
|
||||
group: vouch-manage
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
manage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- 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 }}
|
||||
|
||||
- uses: mitchellh/vouch/action/manage-by-issue@main
|
||||
with:
|
||||
issue-id: ${{ github.event.issue.number }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
883
.opencode/agent/translator.md
Normal file
883
.opencode/agent/translator.md
Normal file
@@ -0,0 +1,883 @@
|
||||
---
|
||||
description: Translate content for a specified locale while preserving technical terms
|
||||
mode: subagent
|
||||
model: opencode/gemini-3-pro
|
||||
---
|
||||
|
||||
You are a professional translator and localization specialist.
|
||||
|
||||
Translate the user's content into the requested target locale (language + region, e.g. fr-FR, de-DE).
|
||||
|
||||
Requirements:
|
||||
|
||||
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
||||
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
||||
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
||||
- Do not modify fenced code blocks.
|
||||
- Output ONLY the translation (no commentary).
|
||||
|
||||
If the target locale is missing, ask the user to provide it.
|
||||
|
||||
---
|
||||
|
||||
# Do-Not-Translate Terms (OpenCode Docs)
|
||||
|
||||
Generated from: `packages/web/src/content/docs/*.mdx` (default English docs)
|
||||
Generated on: 2026-02-10
|
||||
|
||||
Use this as a translation QA checklist / glossary. Preserve listed terms exactly (spelling, casing, punctuation).
|
||||
|
||||
General rules (verbatim, even if not listed below):
|
||||
|
||||
- Anything inside inline code (single backticks) or fenced code blocks (triple backticks)
|
||||
- MDX/JS code in docs: `import ... from "..."`, component tags, identifiers
|
||||
- CLI commands, flags, config keys/values, file paths, URLs/domains, and env vars
|
||||
|
||||
## Proper nouns and product names
|
||||
|
||||
Additional (not reliably captured via link text):
|
||||
|
||||
```text
|
||||
Astro
|
||||
Bun
|
||||
Chocolatey
|
||||
Cursor
|
||||
Docker
|
||||
Git
|
||||
GitHub Actions
|
||||
GitLab CI
|
||||
GNOME Terminal
|
||||
Homebrew
|
||||
Mise
|
||||
Neovim
|
||||
Node.js
|
||||
npm
|
||||
Obsidian
|
||||
opencode
|
||||
opencode-ai
|
||||
Paru
|
||||
pnpm
|
||||
ripgrep
|
||||
Scoop
|
||||
SST
|
||||
Starlight
|
||||
Visual Studio Code
|
||||
VS Code
|
||||
VSCodium
|
||||
Windsurf
|
||||
Windows Terminal
|
||||
Yarn
|
||||
Zellij
|
||||
Zed
|
||||
anomalyco
|
||||
```
|
||||
|
||||
Extracted from link labels in the English docs (review and prune as desired):
|
||||
|
||||
```text
|
||||
@openspoon/subtask2
|
||||
302.AI console
|
||||
ACP progress report
|
||||
Agent Client Protocol
|
||||
Agent Skills
|
||||
Agentic
|
||||
AGENTS.md
|
||||
AI SDK
|
||||
Alacritty
|
||||
Anthropic
|
||||
Anthropic's Data Policies
|
||||
Atom One
|
||||
Avante.nvim
|
||||
Ayu
|
||||
Azure AI Foundry
|
||||
Azure portal
|
||||
Baseten
|
||||
built-in GITHUB_TOKEN
|
||||
Bun.$
|
||||
Catppuccin
|
||||
Cerebras console
|
||||
ChatGPT Plus or Pro
|
||||
Cloudflare dashboard
|
||||
CodeCompanion.nvim
|
||||
CodeNomad
|
||||
Configuring Adapters: Environment Variables
|
||||
Context7 MCP server
|
||||
Cortecs console
|
||||
Deep Infra dashboard
|
||||
DeepSeek console
|
||||
Duo Agent Platform
|
||||
Everforest
|
||||
Fireworks AI console
|
||||
Firmware dashboard
|
||||
Ghostty
|
||||
GitLab CLI agents docs
|
||||
GitLab docs
|
||||
GitLab User Settings > Access Tokens
|
||||
Granular Rules (Object Syntax)
|
||||
Grep by Vercel
|
||||
Groq console
|
||||
Gruvbox
|
||||
Helicone
|
||||
Helicone documentation
|
||||
Helicone Header Directory
|
||||
Helicone's Model Directory
|
||||
Hugging Face Inference Providers
|
||||
Hugging Face settings
|
||||
install WSL
|
||||
IO.NET console
|
||||
JetBrains IDE
|
||||
Kanagawa
|
||||
Kitty
|
||||
MiniMax API Console
|
||||
Models.dev
|
||||
Moonshot AI console
|
||||
Nebius Token Factory console
|
||||
Nord
|
||||
OAuth
|
||||
Ollama integration docs
|
||||
OpenAI's Data Policies
|
||||
OpenChamber
|
||||
OpenCode
|
||||
OpenCode config
|
||||
OpenCode Config
|
||||
OpenCode TUI with the opencode theme
|
||||
OpenCode Web - Active Session
|
||||
OpenCode Web - New Session
|
||||
OpenCode Web - See Servers
|
||||
OpenCode Zen
|
||||
OpenCode-Obsidian
|
||||
OpenRouter dashboard
|
||||
OpenWork
|
||||
OVHcloud panel
|
||||
Pro+ subscription
|
||||
SAP BTP Cockpit
|
||||
Scaleway Console IAM settings
|
||||
Scaleway Generative APIs
|
||||
SDK documentation
|
||||
Sentry MCP server
|
||||
shell API
|
||||
Together AI console
|
||||
Tokyonight
|
||||
Unified Billing
|
||||
Venice AI console
|
||||
Vercel dashboard
|
||||
WezTerm
|
||||
Windows Subsystem for Linux (WSL)
|
||||
WSL
|
||||
WSL (Windows Subsystem for Linux)
|
||||
WSL extension
|
||||
xAI console
|
||||
Z.AI API console
|
||||
Zed
|
||||
ZenMux dashboard
|
||||
Zod
|
||||
```
|
||||
|
||||
## Acronyms and initialisms
|
||||
|
||||
```text
|
||||
ACP
|
||||
AGENTS
|
||||
AI
|
||||
AI21
|
||||
ANSI
|
||||
API
|
||||
AST
|
||||
AWS
|
||||
BTP
|
||||
CD
|
||||
CDN
|
||||
CI
|
||||
CLI
|
||||
CMD
|
||||
CORS
|
||||
DEBUG
|
||||
EKS
|
||||
ERROR
|
||||
FAQ
|
||||
GLM
|
||||
GNOME
|
||||
GPT
|
||||
HTML
|
||||
HTTP
|
||||
HTTPS
|
||||
IAM
|
||||
ID
|
||||
IDE
|
||||
INFO
|
||||
IO
|
||||
IP
|
||||
IRSA
|
||||
JS
|
||||
JSON
|
||||
JSONC
|
||||
K2
|
||||
LLM
|
||||
LM
|
||||
LSP
|
||||
M2
|
||||
MCP
|
||||
MR
|
||||
NET
|
||||
NPM
|
||||
NTLM
|
||||
OIDC
|
||||
OS
|
||||
PAT
|
||||
PATH
|
||||
PHP
|
||||
PR
|
||||
PTY
|
||||
README
|
||||
RFC
|
||||
RPC
|
||||
SAP
|
||||
SDK
|
||||
SKILL
|
||||
SSE
|
||||
SSO
|
||||
TS
|
||||
TTY
|
||||
TUI
|
||||
UI
|
||||
URL
|
||||
US
|
||||
UX
|
||||
VCS
|
||||
VPC
|
||||
VPN
|
||||
VS
|
||||
WARN
|
||||
WSL
|
||||
X11
|
||||
YAML
|
||||
```
|
||||
|
||||
## Code identifiers used in prose (CamelCase, mixedCase)
|
||||
|
||||
```text
|
||||
apiKey
|
||||
AppleScript
|
||||
AssistantMessage
|
||||
baseURL
|
||||
BurntSushi
|
||||
ChatGPT
|
||||
ClangFormat
|
||||
CodeCompanion
|
||||
CodeNomad
|
||||
DeepSeek
|
||||
DefaultV2
|
||||
FileContent
|
||||
FileDiff
|
||||
FileNode
|
||||
fineGrained
|
||||
FormatterStatus
|
||||
GitHub
|
||||
GitLab
|
||||
iTerm2
|
||||
JavaScript
|
||||
JetBrains
|
||||
macOS
|
||||
mDNS
|
||||
MiniMax
|
||||
NeuralNomadsAI
|
||||
NickvanDyke
|
||||
NoeFabris
|
||||
OpenAI
|
||||
OpenAPI
|
||||
OpenChamber
|
||||
OpenCode
|
||||
OpenRouter
|
||||
OpenTUI
|
||||
OpenWork
|
||||
ownUserPermissions
|
||||
PowerShell
|
||||
ProviderAuthAuthorization
|
||||
ProviderAuthMethod
|
||||
ProviderInitError
|
||||
SessionStatus
|
||||
TabItem
|
||||
tokenType
|
||||
ToolIDs
|
||||
ToolList
|
||||
TypeScript
|
||||
typesUrl
|
||||
UserMessage
|
||||
VcsInfo
|
||||
WebView2
|
||||
WezTerm
|
||||
xAI
|
||||
ZenMux
|
||||
```
|
||||
|
||||
## OpenCode CLI commands (as shown in docs)
|
||||
|
||||
```text
|
||||
opencode
|
||||
opencode [project]
|
||||
opencode /path/to/project
|
||||
opencode acp
|
||||
opencode agent [command]
|
||||
opencode agent create
|
||||
opencode agent list
|
||||
opencode attach [url]
|
||||
opencode attach http://10.20.30.40:4096
|
||||
opencode attach http://localhost:4096
|
||||
opencode auth [command]
|
||||
opencode auth list
|
||||
opencode auth login
|
||||
opencode auth logout
|
||||
opencode auth ls
|
||||
opencode export [sessionID]
|
||||
opencode github [command]
|
||||
opencode github install
|
||||
opencode github run
|
||||
opencode import <file>
|
||||
opencode import https://opncd.ai/s/abc123
|
||||
opencode import session.json
|
||||
opencode mcp [command]
|
||||
opencode mcp add
|
||||
opencode mcp auth [name]
|
||||
opencode mcp auth list
|
||||
opencode mcp auth ls
|
||||
opencode mcp auth my-oauth-server
|
||||
opencode mcp auth sentry
|
||||
opencode mcp debug <name>
|
||||
opencode mcp debug my-oauth-server
|
||||
opencode mcp list
|
||||
opencode mcp logout [name]
|
||||
opencode mcp logout my-oauth-server
|
||||
opencode mcp ls
|
||||
opencode models --refresh
|
||||
opencode models [provider]
|
||||
opencode models anthropic
|
||||
opencode run [message..]
|
||||
opencode run Explain the use of context in Go
|
||||
opencode serve
|
||||
opencode serve --cors http://localhost:5173 --cors https://app.example.com
|
||||
opencode serve --hostname 0.0.0.0 --port 4096
|
||||
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
|
||||
opencode session [command]
|
||||
opencode session list
|
||||
opencode stats
|
||||
opencode uninstall
|
||||
opencode upgrade
|
||||
opencode upgrade [target]
|
||||
opencode upgrade v0.1.48
|
||||
opencode web
|
||||
opencode web --cors https://example.com
|
||||
opencode web --hostname 0.0.0.0
|
||||
opencode web --mdns
|
||||
opencode web --mdns --mdns-domain myproject.local
|
||||
opencode web --port 4096
|
||||
opencode web --port 4096 --hostname 0.0.0.0
|
||||
opencode.server.close()
|
||||
```
|
||||
|
||||
## Slash commands and routes
|
||||
|
||||
```text
|
||||
/agent
|
||||
/auth/:id
|
||||
/clear
|
||||
/command
|
||||
/config
|
||||
/config/providers
|
||||
/connect
|
||||
/continue
|
||||
/doc
|
||||
/editor
|
||||
/event
|
||||
/experimental/tool?provider=<p>&model=<m>
|
||||
/experimental/tool/ids
|
||||
/export
|
||||
/file?path=<path>
|
||||
/file/content?path=<p>
|
||||
/file/status
|
||||
/find?pattern=<pat>
|
||||
/find/file
|
||||
/find/file?query=<q>
|
||||
/find/symbol?query=<q>
|
||||
/formatter
|
||||
/global/event
|
||||
/global/health
|
||||
/help
|
||||
/init
|
||||
/instance/dispose
|
||||
/log
|
||||
/lsp
|
||||
/mcp
|
||||
/mnt/
|
||||
/mnt/c/
|
||||
/mnt/d/
|
||||
/models
|
||||
/oc
|
||||
/opencode
|
||||
/path
|
||||
/project
|
||||
/project/current
|
||||
/provider
|
||||
/provider/{id}/oauth/authorize
|
||||
/provider/{id}/oauth/callback
|
||||
/provider/auth
|
||||
/q
|
||||
/quit
|
||||
/redo
|
||||
/resume
|
||||
/session
|
||||
/session/:id
|
||||
/session/:id/abort
|
||||
/session/:id/children
|
||||
/session/:id/command
|
||||
/session/:id/diff
|
||||
/session/:id/fork
|
||||
/session/:id/init
|
||||
/session/:id/message
|
||||
/session/:id/message/:messageID
|
||||
/session/:id/permissions/:permissionID
|
||||
/session/:id/prompt_async
|
||||
/session/:id/revert
|
||||
/session/:id/share
|
||||
/session/:id/shell
|
||||
/session/:id/summarize
|
||||
/session/:id/todo
|
||||
/session/:id/unrevert
|
||||
/session/status
|
||||
/share
|
||||
/summarize
|
||||
/theme
|
||||
/tui
|
||||
/tui/append-prompt
|
||||
/tui/clear-prompt
|
||||
/tui/control/next
|
||||
/tui/control/response
|
||||
/tui/execute-command
|
||||
/tui/open-help
|
||||
/tui/open-models
|
||||
/tui/open-sessions
|
||||
/tui/open-themes
|
||||
/tui/show-toast
|
||||
/tui/submit-prompt
|
||||
/undo
|
||||
/Users/username
|
||||
/Users/username/projects/*
|
||||
/vcs
|
||||
```
|
||||
|
||||
## CLI flags and short options
|
||||
|
||||
```text
|
||||
--agent
|
||||
--attach
|
||||
--command
|
||||
--continue
|
||||
--cors
|
||||
--cwd
|
||||
--days
|
||||
--dir
|
||||
--dry-run
|
||||
--event
|
||||
--file
|
||||
--force
|
||||
--fork
|
||||
--format
|
||||
--help
|
||||
--hostname
|
||||
--hostname 0.0.0.0
|
||||
--keep-config
|
||||
--keep-data
|
||||
--log-level
|
||||
--max-count
|
||||
--mdns
|
||||
--mdns-domain
|
||||
--method
|
||||
--model
|
||||
--models
|
||||
--port
|
||||
--print-logs
|
||||
--project
|
||||
--prompt
|
||||
--refresh
|
||||
--session
|
||||
--share
|
||||
--title
|
||||
--token
|
||||
--tools
|
||||
--verbose
|
||||
--version
|
||||
--wait
|
||||
|
||||
-c
|
||||
-d
|
||||
-f
|
||||
-h
|
||||
-m
|
||||
-n
|
||||
-s
|
||||
-v
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
```text
|
||||
AI_API_URL
|
||||
AI_FLOW_CONTEXT
|
||||
AI_FLOW_EVENT
|
||||
AI_FLOW_INPUT
|
||||
AICORE_DEPLOYMENT_ID
|
||||
AICORE_RESOURCE_GROUP
|
||||
AICORE_SERVICE_KEY
|
||||
ANTHROPIC_API_KEY
|
||||
AWS_ACCESS_KEY_ID
|
||||
AWS_BEARER_TOKEN_BEDROCK
|
||||
AWS_PROFILE
|
||||
AWS_REGION
|
||||
AWS_ROLE_ARN
|
||||
AWS_SECRET_ACCESS_KEY
|
||||
AWS_WEB_IDENTITY_TOKEN_FILE
|
||||
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||
AZURE_RESOURCE_NAME
|
||||
CI_PROJECT_DIR
|
||||
CI_SERVER_FQDN
|
||||
CI_WORKLOAD_REF
|
||||
CLOUDFLARE_ACCOUNT_ID
|
||||
CLOUDFLARE_API_TOKEN
|
||||
CLOUDFLARE_GATEWAY_ID
|
||||
CONTEXT7_API_KEY
|
||||
GITHUB_TOKEN
|
||||
GITLAB_AI_GATEWAY_URL
|
||||
GITLAB_HOST
|
||||
GITLAB_INSTANCE_URL
|
||||
GITLAB_OAUTH_CLIENT_ID
|
||||
GITLAB_TOKEN
|
||||
GITLAB_TOKEN_OPENCODE
|
||||
GOOGLE_APPLICATION_CREDENTIALS
|
||||
GOOGLE_CLOUD_PROJECT
|
||||
HTTP_PROXY
|
||||
HTTPS_PROXY
|
||||
K2_
|
||||
MY_API_KEY
|
||||
MY_ENV_VAR
|
||||
MY_MCP_CLIENT_ID
|
||||
MY_MCP_CLIENT_SECRET
|
||||
NO_PROXY
|
||||
NODE_ENV
|
||||
NODE_EXTRA_CA_CERTS
|
||||
NPM_AUTH_TOKEN
|
||||
OC_ALLOW_WAYLAND
|
||||
OPENCODE_API_KEY
|
||||
OPENCODE_AUTH_JSON
|
||||
OPENCODE_AUTO_SHARE
|
||||
OPENCODE_CLIENT
|
||||
OPENCODE_CONFIG
|
||||
OPENCODE_CONFIG_CONTENT
|
||||
OPENCODE_CONFIG_DIR
|
||||
OPENCODE_DISABLE_AUTOCOMPACT
|
||||
OPENCODE_DISABLE_AUTOUPDATE
|
||||
OPENCODE_DISABLE_CLAUDE_CODE
|
||||
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
||||
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||
OPENCODE_DISABLE_FILETIME_CHECK
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD
|
||||
OPENCODE_DISABLE_MODELS_FETCH
|
||||
OPENCODE_DISABLE_PRUNE
|
||||
OPENCODE_DISABLE_TERMINAL_TITLE
|
||||
OPENCODE_ENABLE_EXA
|
||||
OPENCODE_ENABLE_EXPERIMENTAL_MODELS
|
||||
OPENCODE_EXPERIMENTAL
|
||||
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER
|
||||
OPENCODE_EXPERIMENTAL_EXA
|
||||
OPENCODE_EXPERIMENTAL_FILEWATCHER
|
||||
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY
|
||||
OPENCODE_EXPERIMENTAL_LSP_TOOL
|
||||
OPENCODE_EXPERIMENTAL_LSP_TY
|
||||
OPENCODE_EXPERIMENTAL_MARKDOWN
|
||||
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX
|
||||
OPENCODE_EXPERIMENTAL_OXFMT
|
||||
OPENCODE_EXPERIMENTAL_PLAN_MODE
|
||||
OPENCODE_FAKE_VCS
|
||||
OPENCODE_GIT_BASH_PATH
|
||||
OPENCODE_MODEL
|
||||
OPENCODE_MODELS_URL
|
||||
OPENCODE_PERMISSION
|
||||
OPENCODE_PORT
|
||||
OPENCODE_SERVER_PASSWORD
|
||||
OPENCODE_SERVER_USERNAME
|
||||
PROJECT_ROOT
|
||||
RESOURCE_NAME
|
||||
RUST_LOG
|
||||
VARIABLE_NAME
|
||||
VERTEX_LOCATION
|
||||
XDG_CONFIG_HOME
|
||||
```
|
||||
|
||||
## Package/module identifiers
|
||||
|
||||
```text
|
||||
../../../config.mjs
|
||||
@astrojs/starlight/components
|
||||
@opencode-ai/plugin
|
||||
@opencode-ai/sdk
|
||||
path
|
||||
shescape
|
||||
zod
|
||||
|
||||
@
|
||||
@ai-sdk/anthropic
|
||||
@ai-sdk/cerebras
|
||||
@ai-sdk/google
|
||||
@ai-sdk/openai
|
||||
@ai-sdk/openai-compatible
|
||||
@File#L37-42
|
||||
@modelcontextprotocol/server-everything
|
||||
@opencode
|
||||
```
|
||||
|
||||
## GitHub owner/repo slugs referenced in docs
|
||||
|
||||
```text
|
||||
24601/opencode-zellij-namer
|
||||
angristan/opencode-wakatime
|
||||
anomalyco/opencode
|
||||
apps/opencode-agent
|
||||
athal7/opencode-devcontainers
|
||||
awesome-opencode/awesome-opencode
|
||||
backnotprop/plannotator
|
||||
ben-vargas/ai-sdk-provider-opencode-sdk
|
||||
btriapitsyn/openchamber
|
||||
BurntSushi/ripgrep
|
||||
Cluster444/agentic
|
||||
code-yeongyu/oh-my-opencode
|
||||
darrenhinde/opencode-agents
|
||||
different-ai/opencode-scheduler
|
||||
different-ai/openwork
|
||||
features/copilot
|
||||
folke/tokyonight.nvim
|
||||
franlol/opencode-md-table-formatter
|
||||
ggml-org/llama.cpp
|
||||
ghoulr/opencode-websearch-cited.git
|
||||
H2Shami/opencode-helicone-session
|
||||
hosenur/portal
|
||||
jamesmurdza/daytona
|
||||
jenslys/opencode-gemini-auth
|
||||
JRedeker/opencode-morph-fast-apply
|
||||
JRedeker/opencode-shell-strategy
|
||||
kdcokenny/ocx
|
||||
kdcokenny/opencode-background-agents
|
||||
kdcokenny/opencode-notify
|
||||
kdcokenny/opencode-workspace
|
||||
kdcokenny/opencode-worktree
|
||||
login/device
|
||||
mohak34/opencode-notifier
|
||||
morhetz/gruvbox
|
||||
mtymek/opencode-obsidian
|
||||
NeuralNomadsAI/CodeNomad
|
||||
nick-vi/opencode-type-inject
|
||||
NickvanDyke/opencode.nvim
|
||||
NoeFabris/opencode-antigravity-auth
|
||||
nordtheme/nord
|
||||
numman-ali/opencode-openai-codex-auth
|
||||
olimorris/codecompanion.nvim
|
||||
panta82/opencode-notificator
|
||||
rebelot/kanagawa.nvim
|
||||
remorses/kimaki
|
||||
sainnhe/everforest
|
||||
shekohex/opencode-google-antigravity-auth
|
||||
shekohex/opencode-pty.git
|
||||
spoons-and-mirrors/subtask2
|
||||
sudo-tee/opencode.nvim
|
||||
supermemoryai/opencode-supermemory
|
||||
Tarquinen/opencode-dynamic-context-pruning
|
||||
Th3Whit3Wolf/one-nvim
|
||||
upstash/context7
|
||||
vtemian/micode
|
||||
vtemian/octto
|
||||
yetone/avante.nvim
|
||||
zenobi-us/opencode-plugin-template
|
||||
zenobi-us/opencode-skillful
|
||||
```
|
||||
|
||||
## Paths, filenames, globs, and URLs
|
||||
|
||||
```text
|
||||
./.opencode/themes/*.json
|
||||
./<project-slug>/storage/
|
||||
./config/#custom-directory
|
||||
./global/storage/
|
||||
.agents/skills/*/SKILL.md
|
||||
.agents/skills/<name>/SKILL.md
|
||||
.clang-format
|
||||
.claude
|
||||
.claude/skills
|
||||
.claude/skills/*/SKILL.md
|
||||
.claude/skills/<name>/SKILL.md
|
||||
.env
|
||||
.github/workflows/opencode.yml
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
.ignore
|
||||
.NET SDK
|
||||
.npmrc
|
||||
.ocamlformat
|
||||
.opencode
|
||||
.opencode/
|
||||
.opencode/agents/
|
||||
.opencode/commands/
|
||||
.opencode/commands/test.md
|
||||
.opencode/modes/
|
||||
.opencode/plans/*.md
|
||||
.opencode/plugins/
|
||||
.opencode/skills/<name>/SKILL.md
|
||||
.opencode/skills/git-release/SKILL.md
|
||||
.opencode/tools/
|
||||
.well-known/opencode
|
||||
{ type: "raw" \| "patch", content: string }
|
||||
{file:path/to/file}
|
||||
**/*.js
|
||||
%USERPROFILE%/intelephense/license.txt
|
||||
%USERPROFILE%\.cache\opencode
|
||||
%USERPROFILE%\.config\opencode\opencode.jsonc
|
||||
%USERPROFILE%\.config\opencode\plugins
|
||||
%USERPROFILE%\.local\share\opencode
|
||||
%USERPROFILE%\.local\share\opencode\log
|
||||
<project-root>/.opencode/themes/*.json
|
||||
<providerId>/<modelId>
|
||||
<your-project>/.opencode/plugins/
|
||||
~
|
||||
~/...
|
||||
~/.agents/skills/*/SKILL.md
|
||||
~/.agents/skills/<name>/SKILL.md
|
||||
~/.aws/credentials
|
||||
~/.bashrc
|
||||
~/.cache/opencode
|
||||
~/.cache/opencode/node_modules/
|
||||
~/.claude/CLAUDE.md
|
||||
~/.claude/skills/
|
||||
~/.claude/skills/*/SKILL.md
|
||||
~/.claude/skills/<name>/SKILL.md
|
||||
~/.config/opencode
|
||||
~/.config/opencode/AGENTS.md
|
||||
~/.config/opencode/agents/
|
||||
~/.config/opencode/commands/
|
||||
~/.config/opencode/modes/
|
||||
~/.config/opencode/opencode.json
|
||||
~/.config/opencode/opencode.jsonc
|
||||
~/.config/opencode/plugins/
|
||||
~/.config/opencode/skills/*/SKILL.md
|
||||
~/.config/opencode/skills/<name>/SKILL.md
|
||||
~/.config/opencode/themes/*.json
|
||||
~/.config/opencode/tools/
|
||||
~/.config/zed/settings.json
|
||||
~/.local/share
|
||||
~/.local/share/opencode/
|
||||
~/.local/share/opencode/auth.json
|
||||
~/.local/share/opencode/log/
|
||||
~/.local/share/opencode/mcp-auth.json
|
||||
~/.local/share/opencode/opencode.jsonc
|
||||
~/.npmrc
|
||||
~/.zshrc
|
||||
~/code/
|
||||
~/Library/Application Support
|
||||
~/projects/*
|
||||
~/projects/personal/
|
||||
${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts
|
||||
$HOME/intelephense/license.txt
|
||||
$HOME/projects/*
|
||||
$XDG_CONFIG_HOME/opencode/themes/*.json
|
||||
agent/
|
||||
agents/
|
||||
build/
|
||||
commands/
|
||||
dist/
|
||||
http://<wsl-ip>:4096
|
||||
http://127.0.0.1:8080/callback
|
||||
http://localhost:<port>
|
||||
http://localhost:4096
|
||||
http://localhost:4096/doc
|
||||
https://app.example.com
|
||||
https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/
|
||||
https://opencode.ai/zen/v1/chat/completions
|
||||
https://opencode.ai/zen/v1/messages
|
||||
https://opencode.ai/zen/v1/models/gemini-3-flash
|
||||
https://opencode.ai/zen/v1/models/gemini-3-pro
|
||||
https://opencode.ai/zen/v1/responses
|
||||
https://RESOURCE_NAME.openai.azure.com/
|
||||
laravel/pint
|
||||
log/
|
||||
model: "anthropic/claude-sonnet-4-5"
|
||||
modes/
|
||||
node_modules/
|
||||
openai/gpt-4.1
|
||||
opencode.ai/config.json
|
||||
opencode/<model-id>
|
||||
opencode/gpt-5.1-codex
|
||||
opencode/gpt-5.2-codex
|
||||
opencode/kimi-k2
|
||||
openrouter/google/gemini-2.5-flash
|
||||
opncd.ai/s/<share-id>
|
||||
packages/*/AGENTS.md
|
||||
plugins/
|
||||
project/
|
||||
provider_id/model_id
|
||||
provider/model
|
||||
provider/model-id
|
||||
rm -rf ~/.cache/opencode
|
||||
skills/
|
||||
skills/*/SKILL.md
|
||||
src/**/*.ts
|
||||
themes/
|
||||
tools/
|
||||
```
|
||||
|
||||
## Keybind strings
|
||||
|
||||
```text
|
||||
alt+b
|
||||
Alt+Ctrl+K
|
||||
alt+d
|
||||
alt+f
|
||||
Cmd+Esc
|
||||
Cmd+Option+K
|
||||
Cmd+Shift+Esc
|
||||
Cmd+Shift+G
|
||||
Cmd+Shift+P
|
||||
ctrl+a
|
||||
ctrl+b
|
||||
ctrl+d
|
||||
ctrl+e
|
||||
Ctrl+Esc
|
||||
ctrl+f
|
||||
ctrl+g
|
||||
ctrl+k
|
||||
Ctrl+Shift+Esc
|
||||
Ctrl+Shift+P
|
||||
ctrl+t
|
||||
ctrl+u
|
||||
ctrl+w
|
||||
ctrl+x
|
||||
DELETE
|
||||
Shift+Enter
|
||||
WIN+R
|
||||
```
|
||||
|
||||
## Model ID strings referenced
|
||||
|
||||
```text
|
||||
{env:OPENCODE_MODEL}
|
||||
anthropic/claude-3-5-sonnet-20241022
|
||||
anthropic/claude-haiku-4-20250514
|
||||
anthropic/claude-haiku-4-5
|
||||
anthropic/claude-sonnet-4-20250514
|
||||
anthropic/claude-sonnet-4-5
|
||||
gitlab/duo-chat-haiku-4-5
|
||||
lmstudio/google/gemma-3n-e4b
|
||||
openai/gpt-4.1
|
||||
openai/gpt-5
|
||||
opencode/gpt-5.1-codex
|
||||
opencode/gpt-5.2-codex
|
||||
opencode/kimi-k2
|
||||
openrouter/google/gemini-2.5-flash
|
||||
```
|
||||
@@ -16,15 +16,12 @@ wip:
|
||||
|
||||
For anything in the packages/web use the docs: prefix.
|
||||
|
||||
For anything in the packages/app use the ignore: prefix.
|
||||
|
||||
prefer to explain WHY something was done from an end user perspective instead of
|
||||
WHAT was done.
|
||||
|
||||
do not do generic messages like "improved agent experience" be very specific
|
||||
about what user facing changes were made
|
||||
|
||||
if there are changes do a git pull --rebase
|
||||
if there are conflicts DO NOT FIX THEM. notify me and I will fix them
|
||||
|
||||
## GIT DIFF
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Use this tool to assign and/or label a Github issue.
|
||||
Use this tool to assign and/or label a GitHub issue.
|
||||
|
||||
You can assign the following users:
|
||||
- thdxr
|
||||
|
||||
@@ -110,3 +110,4 @@ const table = sqliteTable("session", {
|
||||
|
||||
- Avoid mocks as much as possible
|
||||
- Test actual implementation, do not duplicate logic into tests
|
||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
|
||||
|
||||
@@ -258,3 +258,49 @@ These are not strictly enforced, they are just general guidelines:
|
||||
## Feature Requests
|
||||
|
||||
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
|
||||
|
||||
## Trust & Vouch System
|
||||
|
||||
This project uses [vouch](https://github.com/mitchellh/vouch) to manage contributor trust. The vouch list is maintained in [`.github/VOUCHED.td`](.github/VOUCHED.td).
|
||||
|
||||
### How it works
|
||||
|
||||
- **Vouched users** are explicitly trusted contributors.
|
||||
- **Denounced users** are explicitly blocked. Issues and pull requests from denounced users are automatically closed. If you have been denounced, you can request to be unvouched by reaching out to a maintainer on [Discord](https://opencode.ai/discord)
|
||||
- **Everyone else** can participate normally — you don't need to be vouched to open issues or PRs.
|
||||
|
||||
### For maintainers
|
||||
|
||||
Collaborators with write access can manage the vouch list by commenting on any issue:
|
||||
|
||||
- `vouch` — vouch for the issue author
|
||||
- `vouch @username` — vouch for a specific user
|
||||
- `denounce` — denounce the issue author
|
||||
- `denounce @username` — denounce a specific user
|
||||
- `denounce @username <reason>` — denounce with a reason
|
||||
- `unvouch` / `unvouch @username` — remove someone from the list
|
||||
|
||||
Changes are committed automatically to `.github/VOUCHED.td`.
|
||||
|
||||
### Denouncement policy
|
||||
|
||||
Denouncement is reserved for users who repeatedly submit low-quality AI-generated contributions, spam, or otherwise act in bad faith. It is not used for disagreements or honest mistakes.
|
||||
|
||||
## Issue Requirements
|
||||
|
||||
All issues **must** use one of our issue templates:
|
||||
|
||||
- **Bug report** — for reporting bugs (requires a description)
|
||||
- **Feature request** — for suggesting enhancements (requires verification checkbox and description)
|
||||
- **Question** — for asking questions (requires the question)
|
||||
|
||||
Blank issues are not allowed. When a new issue is opened, an automated check verifies that it follows a template and meets our contributing guidelines. If an issue doesn't meet the requirements, you'll receive a comment explaining what needs to be fixed and have **2 hours** to edit the issue. After that, it will be automatically closed.
|
||||
|
||||
Issues may be flagged for:
|
||||
|
||||
- Not using a template
|
||||
- Required fields left empty or filled with placeholder text
|
||||
- AI-generated walls of text
|
||||
- Missing meaningful content
|
||||
|
||||
If you believe your issue was incorrectly flagged, let a maintainer know.
|
||||
|
||||
@@ -275,7 +275,7 @@ async function assertOpencodeConnected() {
|
||||
body: {
|
||||
service: "github-workflow",
|
||||
level: "info",
|
||||
message: "Prepare to react to Github Workflow event",
|
||||
message: "Prepare to react to GitHub Workflow event",
|
||||
},
|
||||
})
|
||||
connected = true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-1IpZnnN6+acCcV0AgO4OVdvgf4TFBFId5dms5W5ecA0=",
|
||||
"aarch64-linux": "sha256-TKmPhXokOav46ucP9AFwHGgKmB9CdGCcUtwqUtLlzG4=",
|
||||
"aarch64-darwin": "sha256-xJQuw3+QHYnlClDrafQKPQyR+aqyAEofvYYjCowHDps=",
|
||||
"x86_64-darwin": "sha256-ywU3Oka2QNGKu/HI+//3bdYJ9qo1N7K5Wr2vpTgSM/g="
|
||||
"x86_64-linux": "sha256-cvRBvHRuunNjF07c4GVHl5rRgoTn1qfI/HdJWtOV63M=",
|
||||
"aarch64-linux": "sha256-DJUI4pMZ7wQTnyOiuDHALmZz7FZtrTbzRzCuNOShmWE=",
|
||||
"aarch64-darwin": "sha256-JnkqDwuC7lNsjafV+jOGfvs8K1xC8rk5CTOW+spjiCA=",
|
||||
"x86_64-darwin": "sha256-GBeTqq2vDn/mXplYNglrAT2xajjFVzB4ATHnMS0j7z4="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ stdenvNoCC.mkDerivation {
|
||||
../bun.lock
|
||||
../package.json
|
||||
../patches
|
||||
../install
|
||||
../install # required by desktop build (cli.rs include_str!)
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-beta.12-a5629fb",
|
||||
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
||||
"ai": "5.0.124",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
[test]
|
||||
root = "./src"
|
||||
preload = ["./happydom.ts"]
|
||||
|
||||
140
packages/app/e2e/projects/workspace-new-session.spec.ts
Normal file
140
packages/app/e2e/projects/workspace-new-session.spec.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import type { Page } from "@playwright/test"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { cleanupTestProject, openSidebar, sessionIDFromUrl, setWorkspacesEnabled } from "../actions"
|
||||
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||
import { createSdk } from "../utils"
|
||||
|
||||
function slugFromUrl(url: string) {
|
||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||
}
|
||||
|
||||
async function waitWorkspaceReady(page: Page, slug: string) {
|
||||
await openSidebar(page)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||
try {
|
||||
await item.hover({ timeout: 500 })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
}
|
||||
|
||||
async function createWorkspace(page: Page, root: string, seen: string[]) {
|
||||
await openSidebar(page)
|
||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const slug = slugFromUrl(page.url())
|
||||
if (!slug) return ""
|
||||
if (slug === root) return ""
|
||||
if (seen.includes(slug)) return ""
|
||||
return slug
|
||||
},
|
||||
{ timeout: 45_000 },
|
||||
)
|
||||
.not.toBe("")
|
||||
|
||||
const slug = slugFromUrl(page.url())
|
||||
const directory = base64Decode(slug)
|
||||
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
|
||||
return { slug, directory }
|
||||
}
|
||||
|
||||
async function openWorkspaceNewSession(page: Page, slug: string) {
|
||||
await waitWorkspaceReady(page, slug)
|
||||
|
||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||
await item.hover()
|
||||
|
||||
const button = page.locator(workspaceNewSessionSelector(slug)).first()
|
||||
await expect(button).toBeVisible()
|
||||
await button.click({ force: true })
|
||||
|
||||
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session(?:[/?#]|$)`))
|
||||
}
|
||||
|
||||
async function createSessionFromWorkspace(page: Page, slug: string, text: string) {
|
||||
await openWorkspaceNewSession(page, slug)
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await expect(prompt).toBeVisible()
|
||||
await prompt.click()
|
||||
await page.keyboard.type(text)
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/[^/?#]+`), { timeout: 30_000 })
|
||||
|
||||
const sessionID = sessionIDFromUrl(page.url())
|
||||
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
||||
return sessionID
|
||||
}
|
||||
|
||||
async function sessionDirectory(directory: string, sessionID: string) {
|
||||
const info = await createSdk(directory)
|
||||
.session.get({ sessionID })
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
if (!info) return ""
|
||||
return info.directory
|
||||
}
|
||||
|
||||
test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
await withProject(async ({ directory, slug: root }) => {
|
||||
const workspaces = [] as { slug: string; directory: string }[]
|
||||
const sessions = [] as string[]
|
||||
|
||||
try {
|
||||
await openSidebar(page)
|
||||
await setWorkspacesEnabled(page, root, true)
|
||||
|
||||
const first = await createWorkspace(page, root, [])
|
||||
workspaces.push(first)
|
||||
await waitWorkspaceReady(page, first.slug)
|
||||
|
||||
const second = await createWorkspace(page, root, [first.slug])
|
||||
workspaces.push(second)
|
||||
await waitWorkspaceReady(page, second.slug)
|
||||
|
||||
const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`)
|
||||
sessions.push(firstSession)
|
||||
|
||||
const secondSession = await createSessionFromWorkspace(page, second.slug, `workspace two ${Date.now()}`)
|
||||
sessions.push(secondSession)
|
||||
|
||||
const thirdSession = await createSessionFromWorkspace(page, first.slug, `workspace one again ${Date.now()}`)
|
||||
sessions.push(thirdSession)
|
||||
|
||||
await expect.poll(() => sessionDirectory(first.directory, firstSession)).toBe(first.directory)
|
||||
await expect.poll(() => sessionDirectory(second.directory, secondSession)).toBe(second.directory)
|
||||
await expect.poll(() => sessionDirectory(first.directory, thirdSession)).toBe(first.directory)
|
||||
} finally {
|
||||
const dirs = [directory, ...workspaces.map((workspace) => workspace.directory)]
|
||||
await Promise.all(
|
||||
sessions.map((sessionID) =>
|
||||
Promise.all(
|
||||
dirs.map((dir) =>
|
||||
createSdk(dir)
|
||||
.session.delete({ sessionID })
|
||||
.catch(() => undefined),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
await Promise.all(workspaces.map((workspace) => cleanupTestProject(workspace.directory)))
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -48,6 +48,9 @@ export const workspaceItemSelector = (slug: string) =>
|
||||
export const workspaceMenuTriggerSelector = (slug: string) =>
|
||||
`${sidebarNavSelector} [data-action="workspace-menu"][data-workspace="${slug}"]`
|
||||
|
||||
export const workspaceNewSessionSelector = (slug: string) =>
|
||||
`${sidebarNavSelector} [data-action="workspace-new-session"][data-workspace="${slug}"]`
|
||||
|
||||
export const listItemSelector = '[data-slot="list-item"]'
|
||||
|
||||
export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]`
|
||||
|
||||
126
packages/app/e2e/session/session-undo-redo.spec.ts
Normal file
126
packages/app/e2e/session/session-undo-redo.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { Page } from "@playwright/test"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { withSession } from "../actions"
|
||||
import { createSdk, modKey } from "../utils"
|
||||
import { promptSelector } from "../selectors"
|
||||
|
||||
async function seedConversation(input: {
|
||||
page: Page
|
||||
sdk: ReturnType<typeof createSdk>
|
||||
sessionID: string
|
||||
token: string
|
||||
}) {
|
||||
const prompt = input.page.locator(promptSelector)
|
||||
await expect(prompt).toBeVisible()
|
||||
await prompt.click()
|
||||
await input.page.keyboard.type(`Reply with exactly: ${input.token}`)
|
||||
await input.page.keyboard.press("Enter")
|
||||
|
||||
let userMessageID: string | undefined
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const messages = await input.sdk.session
|
||||
.messages({ sessionID: input.sessionID, limit: 50 })
|
||||
.then((r) => r.data ?? [])
|
||||
const users = messages.filter((m) => m.info.role === "user")
|
||||
if (users.length === 0) return false
|
||||
|
||||
const user = users.reduce((acc, item) => (item.info.id > acc.info.id ? item : acc))
|
||||
userMessageID = user.info.id
|
||||
|
||||
const assistantText = messages
|
||||
.filter((m) => m.info.role === "assistant")
|
||||
.flatMap((m) => m.parts)
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")
|
||||
|
||||
return assistantText.includes(input.token)
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
if (!userMessageID) throw new Error("Expected a user message id")
|
||||
return { prompt, userMessageID }
|
||||
}
|
||||
|
||||
test("slash undo sets revert and restores prior prompt", async ({ page, withProject }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
const token = `undo_${Date.now()}`
|
||||
|
||||
await withProject(async (project) => {
|
||||
const sdk = createSdk(project.directory)
|
||||
|
||||
await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
|
||||
await project.gotoSession(session.id)
|
||||
|
||||
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
|
||||
|
||||
await seeded.prompt.click()
|
||||
await page.keyboard.type("/undo")
|
||||
|
||||
const undo = page.locator('[data-slash-id="session.undo"]').first()
|
||||
await expect(undo).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect
|
||||
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.toBe(seeded.userMessageID)
|
||||
|
||||
await expect(seeded.prompt).toContainText(token)
|
||||
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("slash redo clears revert and restores latest state", async ({ page, withProject }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
const token = `redo_${Date.now()}`
|
||||
|
||||
await withProject(async (project) => {
|
||||
const sdk = createSdk(project.directory)
|
||||
|
||||
await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
|
||||
await project.gotoSession(session.id)
|
||||
|
||||
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
|
||||
|
||||
await seeded.prompt.click()
|
||||
await page.keyboard.type("/undo")
|
||||
|
||||
const undo = page.locator('[data-slash-id="session.undo"]').first()
|
||||
await expect(undo).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect
|
||||
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.toBe(seeded.userMessageID)
|
||||
|
||||
await seeded.prompt.click()
|
||||
await page.keyboard.press(`${modKey}+A`)
|
||||
await page.keyboard.press("Backspace")
|
||||
await page.keyboard.type("/redo")
|
||||
|
||||
const redo = page.locator('[data-slash-id="session.redo"]').first()
|
||||
await expect(redo).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect
|
||||
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.toBeUndefined()
|
||||
|
||||
await expect(seeded.prompt).not.toContainText(token)
|
||||
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`).first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -89,7 +89,6 @@ let runner: ReturnType<typeof Bun.spawn> | undefined
|
||||
let server: { stop: () => Promise<void> | void } | undefined
|
||||
let inst: { Instance: { disposeAll: () => Promise<void> | void } } | undefined
|
||||
let cleaned = false
|
||||
let internalError = false
|
||||
|
||||
const cleanup = async () => {
|
||||
if (cleaned) return
|
||||
@@ -115,9 +114,8 @@ const shutdown = (code: number, reason: string) => {
|
||||
}
|
||||
|
||||
const reportInternalError = (reason: string, error: unknown) => {
|
||||
internalError = true
|
||||
console.error(`e2e-local internal error: ${reason}`)
|
||||
console.error(error)
|
||||
console.warn(`e2e-local ignored server error: ${reason}`)
|
||||
console.warn(error)
|
||||
}
|
||||
|
||||
process.once("SIGINT", () => shutdown(130, "SIGINT"))
|
||||
@@ -177,6 +175,4 @@ try {
|
||||
await cleanup()
|
||||
}
|
||||
|
||||
if (code === 0 && internalError) code = 1
|
||||
|
||||
process.exit(code)
|
||||
|
||||
@@ -6,6 +6,7 @@ let dirsToExpand: typeof import("./file-tree").dirsToExpand
|
||||
|
||||
beforeAll(async () => {
|
||||
mock.module("@solidjs/router", () => ({
|
||||
useNavigate: () => () => undefined,
|
||||
useParams: () => ({}),
|
||||
}))
|
||||
mock.module("@/context/file", () => ({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useFile } from "@/context/file"
|
||||
import { encodeFilePath } from "@/context/file/path"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
@@ -20,11 +21,7 @@ import { Dynamic } from "solid-js/web"
|
||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||
|
||||
function pathToFileUrl(filepath: string): string {
|
||||
const encodedPath = filepath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
return `file://${encodedPath}`
|
||||
return `file://${encodeFilePath(filepath)}`
|
||||
}
|
||||
|
||||
type Kind = "add" | "del" | "mix"
|
||||
@@ -223,12 +220,14 @@ export default function FileTree(props: {
|
||||
seen.add(item)
|
||||
}
|
||||
|
||||
return out.toSorted((a, b) => {
|
||||
out.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === "directory" ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
return out
|
||||
})
|
||||
|
||||
const Node = (
|
||||
|
||||
@@ -787,7 +787,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
},
|
||||
setMode: (mode) => setStore("mode", mode),
|
||||
setPopover: (popover) => setStore("popover", popover),
|
||||
newSessionWorktree: props.newSessionWorktree,
|
||||
newSessionWorktree: () => props.newSessionWorktree,
|
||||
onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
|
||||
onSubmit: props.onSubmit,
|
||||
})
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("buildRequestParts", () => {
|
||||
// Special chars should be encoded
|
||||
expect(filePart.url).toContain("file%23name.txt")
|
||||
// Should have Windows drive letter properly encoded
|
||||
expect(filePart.url).toMatch(/file:\/\/\/[A-Z]%3A/)
|
||||
expect(filePart.url).toMatch(/file:\/\/\/[A-Z]:/)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -210,7 +210,7 @@ describe("buildRequestParts", () => {
|
||||
if (filePart?.type === "file") {
|
||||
// Should handle absolute path that differs from sessionDirectory
|
||||
expect(() => new URL(filePart.url)).not.toThrow()
|
||||
expect(filePart.url).toContain("/D%3A/other/project/file.ts")
|
||||
expect(filePart.url).toContain("/D:/other/project/file.ts")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
|
||||
import type { FileSelection } from "@/context/file"
|
||||
import { encodeFilePath } from "@/context/file/path"
|
||||
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
|
||||
import { Identifier } from "@/utils/id"
|
||||
|
||||
@@ -27,23 +28,11 @@ type BuildRequestPartsInput = {
|
||||
sessionDirectory: string
|
||||
}
|
||||
|
||||
const absolute = (directory: string, path: string) =>
|
||||
path.startsWith("/") ? path : (directory + "/" + path).replace("//", "/")
|
||||
|
||||
const encodeFilePath = (filepath: string): string => {
|
||||
// Normalize Windows paths: convert backslashes to forward slashes
|
||||
let normalized = filepath.replace(/\\/g, "/")
|
||||
|
||||
// Handle Windows absolute paths (D:/path -> /D:/path for proper file:// URLs)
|
||||
if (/^[A-Za-z]:/.test(normalized)) {
|
||||
normalized = "/" + normalized
|
||||
}
|
||||
|
||||
// Encode each path segment (preserving forward slashes as path separators)
|
||||
return normalized
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
const absolute = (directory: string, path: string) => {
|
||||
if (path.startsWith("/")) return path
|
||||
if (/^[A-Za-z]:[\\/]/.test(path) || /^[A-Za-z]:$/.test(path)) return path
|
||||
if (path.startsWith("\\\\") || path.startsWith("//")) return path
|
||||
return `${directory.replace(/[\\/]+$/, "")}/${path}`
|
||||
}
|
||||
|
||||
const fileQuery = (selection: FileSelection | undefined) =>
|
||||
|
||||
175
packages/app/src/components/prompt-input/submit.test.ts
Normal file
175
packages/app/src/components/prompt-input/submit.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"
|
||||
import type { Prompt } from "@/context/prompt"
|
||||
|
||||
let createPromptSubmit: typeof import("./submit").createPromptSubmit
|
||||
|
||||
const createdClients: string[] = []
|
||||
const createdSessions: string[] = []
|
||||
const sentShell: string[] = []
|
||||
const syncedDirectories: string[] = []
|
||||
|
||||
let selected = "/repo/worktree-a"
|
||||
|
||||
const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
||||
|
||||
const clientFor = (directory: string) => ({
|
||||
session: {
|
||||
create: async () => {
|
||||
createdSessions.push(directory)
|
||||
return { data: { id: `session-${createdSessions.length}` } }
|
||||
},
|
||||
shell: async () => {
|
||||
sentShell.push(directory)
|
||||
return { data: undefined }
|
||||
},
|
||||
prompt: async () => ({ data: undefined }),
|
||||
command: async () => ({ data: undefined }),
|
||||
abort: async () => ({ data: undefined }),
|
||||
},
|
||||
worktree: {
|
||||
create: async () => ({ data: { directory: `${directory}/new` } }),
|
||||
},
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
const rootClient = clientFor("/repo/main")
|
||||
|
||||
mock.module("@solidjs/router", () => ({
|
||||
useNavigate: () => () => undefined,
|
||||
useParams: () => ({}),
|
||||
}))
|
||||
|
||||
mock.module("@opencode-ai/sdk/v2/client", () => ({
|
||||
createOpencodeClient: (input: { directory: string }) => {
|
||||
createdClients.push(input.directory)
|
||||
return clientFor(input.directory)
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module("@opencode-ai/ui/toast", () => ({
|
||||
showToast: () => 0,
|
||||
}))
|
||||
|
||||
mock.module("@opencode-ai/util/encode", () => ({
|
||||
base64Encode: (value: string) => value,
|
||||
}))
|
||||
|
||||
mock.module("@/context/local", () => ({
|
||||
useLocal: () => ({
|
||||
model: {
|
||||
current: () => ({ id: "model", provider: { id: "provider" } }),
|
||||
variant: { current: () => undefined },
|
||||
},
|
||||
agent: {
|
||||
current: () => ({ name: "agent" }),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/prompt", () => ({
|
||||
usePrompt: () => ({
|
||||
current: () => promptValue,
|
||||
reset: () => undefined,
|
||||
set: () => undefined,
|
||||
context: {
|
||||
add: () => undefined,
|
||||
remove: () => undefined,
|
||||
items: () => [],
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/layout", () => ({
|
||||
useLayout: () => ({
|
||||
handoff: {
|
||||
setTabs: () => undefined,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/sdk", () => ({
|
||||
useSDK: () => ({
|
||||
directory: "/repo/main",
|
||||
client: rootClient,
|
||||
url: "http://localhost:4096",
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/sync", () => ({
|
||||
useSync: () => ({
|
||||
data: { command: [] },
|
||||
session: {
|
||||
optimistic: {
|
||||
add: () => undefined,
|
||||
remove: () => undefined,
|
||||
},
|
||||
},
|
||||
set: () => undefined,
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/global-sync", () => ({
|
||||
useGlobalSync: () => ({
|
||||
child: (directory: string) => {
|
||||
syncedDirectories.push(directory)
|
||||
return [{}, () => undefined]
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/platform", () => ({
|
||||
usePlatform: () => ({
|
||||
fetch: fetch,
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/language", () => ({
|
||||
useLanguage: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mod = await import("./submit")
|
||||
createPromptSubmit = mod.createPromptSubmit
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
createdClients.length = 0
|
||||
createdSessions.length = 0
|
||||
sentShell.length = 0
|
||||
syncedDirectories.length = 0
|
||||
selected = "/repo/worktree-a"
|
||||
})
|
||||
|
||||
describe("prompt submit worktree selection", () => {
|
||||
test("reads the latest worktree accessor value per submit", async () => {
|
||||
const submit = createPromptSubmit({
|
||||
info: () => undefined,
|
||||
imageAttachments: () => [],
|
||||
commentCount: () => 0,
|
||||
mode: () => "shell",
|
||||
working: () => false,
|
||||
editor: () => undefined,
|
||||
queueScroll: () => undefined,
|
||||
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||
addToHistory: () => undefined,
|
||||
resetHistoryNavigation: () => undefined,
|
||||
setMode: () => undefined,
|
||||
setPopover: () => undefined,
|
||||
newSessionWorktree: () => selected,
|
||||
onNewSessionWorktreeReset: () => undefined,
|
||||
onSubmit: () => undefined,
|
||||
})
|
||||
|
||||
const event = { preventDefault: () => undefined } as unknown as Event
|
||||
|
||||
await submit.handleSubmit(event)
|
||||
selected = "/repo/worktree-b"
|
||||
await submit.handleSubmit(event)
|
||||
|
||||
expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||
expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||
expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||
})
|
||||
})
|
||||
@@ -37,7 +37,7 @@ type PromptSubmitInput = {
|
||||
resetHistoryNavigation: () => void
|
||||
setMode: (mode: "normal" | "shell") => void
|
||||
setPopover: (popover: "at" | "slash" | null) => void
|
||||
newSessionWorktree?: string
|
||||
newSessionWorktree?: Accessor<string | undefined>
|
||||
onNewSessionWorktreeReset?: () => void
|
||||
onSubmit?: () => void
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
|
||||
const projectDirectory = sdk.directory
|
||||
const isNewSession = !params.id
|
||||
const worktreeSelection = input.newSessionWorktree ?? "main"
|
||||
const worktreeSelection = input.newSessionWorktree?.() || "main"
|
||||
|
||||
let sessionDirectory = projectDirectory
|
||||
let client = sdk.client
|
||||
|
||||
@@ -112,21 +112,35 @@ export function SessionHeader() {
|
||||
|
||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ finder: true })
|
||||
|
||||
const apps = createMemo(() => {
|
||||
if (os() === "macos") return MAC_APPS
|
||||
if (os() === "windows") return WINDOWS_APPS
|
||||
return LINUX_APPS
|
||||
})
|
||||
|
||||
const fileManager = createMemo(() => {
|
||||
if (os() === "macos") return { label: "Finder", icon: "finder" as const }
|
||||
if (os() === "windows") return { label: "File Explorer", icon: "file-explorer" as const }
|
||||
return { label: "File Manager", icon: "finder" as const }
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (platform.platform !== "desktop") return
|
||||
if (!platform.checkAppExists) return
|
||||
|
||||
const list = os()
|
||||
const apps = list === "macos" ? MAC_APPS : list === "windows" ? WINDOWS_APPS : list === "linux" ? LINUX_APPS : []
|
||||
if (apps.length === 0) return
|
||||
const list = apps()
|
||||
|
||||
setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial<Record<OpenApp, boolean>>)
|
||||
|
||||
void Promise.all(
|
||||
apps.map((app) =>
|
||||
Promise.resolve(platform.checkAppExists?.(app.openWith)).then((value) => {
|
||||
const ok = Boolean(value)
|
||||
console.debug(`[session-header] App "${app.label}" (${app.openWith}): ${ok ? "exists" : "does not exist"}`)
|
||||
return [app.id, ok] as const
|
||||
}),
|
||||
list.map((app) =>
|
||||
Promise.resolve(platform.checkAppExists?.(app.openWith))
|
||||
.then((value) => Boolean(value))
|
||||
.catch(() => false)
|
||||
.then((ok) => {
|
||||
console.debug(`[session-header] App "${app.label}" (${app.openWith}): ${ok ? "exists" : "does not exist"}`)
|
||||
return [app.id, ok] as const
|
||||
}),
|
||||
),
|
||||
).then((entries) => {
|
||||
setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>)
|
||||
@@ -134,23 +148,23 @@ export function SessionHeader() {
|
||||
})
|
||||
|
||||
const options = createMemo(() => {
|
||||
if (os() === "macos") {
|
||||
return [{ id: "finder", label: "Finder", icon: "finder" }, ...MAC_APPS.filter((app) => exists[app.id])] as const
|
||||
}
|
||||
|
||||
if (os() === "windows") {
|
||||
return [
|
||||
{ id: "finder", label: "File Explorer", icon: "file-explorer" },
|
||||
...WINDOWS_APPS.filter((app) => exists[app.id]),
|
||||
] as const
|
||||
}
|
||||
|
||||
return [
|
||||
{ id: "finder", label: "File Manager", icon: "finder" },
|
||||
...LINUX_APPS.filter((app) => exists[app.id]),
|
||||
{ id: "finder", label: fileManager().label, icon: fileManager().icon },
|
||||
...apps().filter((app) => exists[app.id]),
|
||||
] as const
|
||||
})
|
||||
|
||||
type OpenIcon = OpenApp | "file-explorer"
|
||||
const base = new Set<OpenIcon>(["finder", "vscode", "cursor", "zed"])
|
||||
const size = (id: OpenIcon) => (base.has(id) ? "size-4" : "size-[19px]")
|
||||
|
||||
const checksReady = createMemo(() => {
|
||||
if (platform.platform !== "desktop") return true
|
||||
if (!platform.checkAppExists) return true
|
||||
const list = apps()
|
||||
return list.every((app) => exists[app.id] !== undefined)
|
||||
})
|
||||
|
||||
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
||||
|
||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||
@@ -158,6 +172,7 @@ export function SessionHeader() {
|
||||
|
||||
createEffect(() => {
|
||||
if (platform.platform !== "desktop") return
|
||||
if (!checksReady()) return
|
||||
const value = prefs.app
|
||||
if (options().some((o) => o.id === value)) return
|
||||
setPrefs("app", options()[0]?.id ?? "finder")
|
||||
@@ -334,11 +349,13 @@ export function SessionHeader() {
|
||||
onClick={() => openDir(current().id)}
|
||||
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
||||
>
|
||||
<AppIcon id={current().icon} class="size-4" />
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<AppIcon id={current().icon} class="size-4" />
|
||||
</div>
|
||||
<span class="text-12-regular text-text-strong">Open</span>
|
||||
</Button>
|
||||
<div class="self-stretch w-px bg-border-base/70" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenu gutter={6} placement="bottom-end">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="chevron-down"
|
||||
@@ -347,7 +364,7 @@ export function SessionHeader() {
|
||||
aria-label={language.t("session.header.open.menu")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content placement="bottom-end" gutter={6}>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
||||
<DropdownMenu.RadioGroup
|
||||
@@ -359,7 +376,9 @@ export function SessionHeader() {
|
||||
>
|
||||
{options().map((o) => (
|
||||
<DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
|
||||
<AppIcon id={o.icon} class="size-5" />
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<AppIcon id={o.icon} class={size(o.icon)} />
|
||||
</div>
|
||||
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<Icon name="check-small" size="small" class="text-icon-weak" />
|
||||
@@ -370,7 +389,9 @@ export function SessionHeader() {
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={copyPath}>
|
||||
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||
</div>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("session.header.open.copyPath")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Show, createMemo } from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
@@ -15,6 +16,7 @@ interface NewSessionViewProps {
|
||||
|
||||
export function NewSessionView(props: NewSessionViewProps) {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const language = useLanguage()
|
||||
|
||||
const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
|
||||
@@ -24,11 +26,11 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
if (options().includes(selection)) return selection
|
||||
return MAIN_WORKTREE
|
||||
})
|
||||
const projectRoot = createMemo(() => sync.project?.worktree ?? sync.data.path.directory)
|
||||
const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory)
|
||||
const isWorktree = createMemo(() => {
|
||||
const project = sync.project
|
||||
if (!project) return false
|
||||
return sync.data.path.directory !== project.worktree
|
||||
return sdk.directory !== project.worktree
|
||||
})
|
||||
|
||||
const label = (value: string) => {
|
||||
@@ -45,7 +47,7 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6 pb-[calc(var(--prompt-height,11.25rem)+64px)]">
|
||||
<div class="size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto 2xl:max-w-[1000px] px-6 pb-[calc(var(--prompt-height,11.25rem)+64px)]">
|
||||
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
|
||||
@@ -74,7 +74,9 @@ export const Terminal = (props: TerminalProps) => {
|
||||
let handleTextareaBlur: () => void
|
||||
let disposed = false
|
||||
const cleanups: VoidFunction[] = []
|
||||
let tail = local.pty.tail ?? ""
|
||||
const start =
|
||||
typeof local.pty.cursor === "number" && Number.isSafeInteger(local.pty.cursor) ? local.pty.cursor : undefined
|
||||
let cursor = start ?? 0
|
||||
|
||||
const cleanup = () => {
|
||||
if (!cleanups.length) return
|
||||
@@ -164,13 +166,16 @@ export const Terminal = (props: TerminalProps) => {
|
||||
|
||||
const once = { value: false }
|
||||
|
||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
|
||||
url.searchParams.set("directory", sdk.directory)
|
||||
url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
|
||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
||||
if (window.__OPENCODE__?.serverPassword) {
|
||||
url.username = "opencode"
|
||||
url.password = window.__OPENCODE__?.serverPassword
|
||||
}
|
||||
const socket = new WebSocket(url)
|
||||
socket.binaryType = "arraybuffer"
|
||||
cleanups.push(() => {
|
||||
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
|
||||
})
|
||||
@@ -289,26 +294,6 @@ export const Terminal = (props: TerminalProps) => {
|
||||
handleResize = () => fit.fit()
|
||||
window.addEventListener("resize", handleResize)
|
||||
cleanups.push(() => window.removeEventListener("resize", handleResize))
|
||||
const limit = 16_384
|
||||
const min = 32
|
||||
const windowMs = 750
|
||||
const seed = tail.length > limit ? tail.slice(-limit) : tail
|
||||
let sync = seed.length >= min
|
||||
let syncUntil = 0
|
||||
const stopSync = () => {
|
||||
sync = false
|
||||
syncUntil = 0
|
||||
}
|
||||
|
||||
const overlap = (data: string) => {
|
||||
if (!seed) return 0
|
||||
const max = Math.min(seed.length, data.length)
|
||||
if (max < min) return 0
|
||||
for (let i = max; i >= min; i--) {
|
||||
if (seed.slice(-i) === data.slice(0, i)) return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const onResize = t.onResize(async (size) => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
@@ -325,7 +310,6 @@ export const Terminal = (props: TerminalProps) => {
|
||||
})
|
||||
cleanups.push(() => disposeIfDisposable(onResize))
|
||||
const onData = t.onData((data) => {
|
||||
if (data) stopSync()
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(data)
|
||||
}
|
||||
@@ -343,7 +327,6 @@ export const Terminal = (props: TerminalProps) => {
|
||||
|
||||
const handleOpen = () => {
|
||||
local.onConnect?.()
|
||||
if (sync) syncUntil = Date.now() + windowMs
|
||||
sdk.client.pty
|
||||
.update({
|
||||
ptyID: local.pty.id,
|
||||
@@ -357,31 +340,31 @@ export const Terminal = (props: TerminalProps) => {
|
||||
socket.addEventListener("open", handleOpen)
|
||||
cleanups.push(() => socket.removeEventListener("open", handleOpen))
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (disposed) return
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
// WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }).
|
||||
const bytes = new Uint8Array(event.data)
|
||||
if (bytes[0] !== 0) return
|
||||
const json = decoder.decode(bytes.subarray(1))
|
||||
try {
|
||||
const meta = JSON.parse(json) as { cursor?: unknown }
|
||||
const next = meta?.cursor
|
||||
if (typeof next === "number" && Number.isSafeInteger(next) && next >= 0) {
|
||||
cursor = next
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const data = typeof event.data === "string" ? event.data : ""
|
||||
if (!data) return
|
||||
|
||||
const next = (() => {
|
||||
if (!sync) return data
|
||||
if (syncUntil && Date.now() > syncUntil) {
|
||||
stopSync()
|
||||
return data
|
||||
}
|
||||
const n = overlap(data)
|
||||
if (!n) {
|
||||
stopSync()
|
||||
return data
|
||||
}
|
||||
const trimmed = data.slice(n)
|
||||
if (trimmed) stopSync()
|
||||
return trimmed
|
||||
})()
|
||||
|
||||
if (!next) return
|
||||
|
||||
t.write(next)
|
||||
tail = next.length >= limit ? next.slice(-limit) : (tail + next).slice(-limit)
|
||||
t.write(data)
|
||||
cursor += data.length
|
||||
}
|
||||
socket.addEventListener("message", handleMessage)
|
||||
cleanups.push(() => socket.removeEventListener("message", handleMessage))
|
||||
@@ -435,7 +418,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
props.onCleanup({
|
||||
...local.pty,
|
||||
buffer,
|
||||
tail,
|
||||
cursor,
|
||||
rows: t.rows,
|
||||
cols: t.cols,
|
||||
scrollY: t.getViewportY(),
|
||||
|
||||
@@ -68,12 +68,14 @@ export function Titlebar() {
|
||||
id: "common.goBack",
|
||||
title: language.t("common.goBack"),
|
||||
category: language.t("command.category.view"),
|
||||
keybind: "mod+[",
|
||||
onSelect: back,
|
||||
},
|
||||
{
|
||||
id: "common.goForward",
|
||||
title: language.t("common.goForward"),
|
||||
category: language.t("command.category.view"),
|
||||
keybind: "mod+]",
|
||||
onSelect: forward,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -6,6 +6,7 @@ let createCommentSessionForTest: typeof import("./comments").createCommentSessio
|
||||
|
||||
beforeAll(async () => {
|
||||
mock.module("@solidjs/router", () => ({
|
||||
useNavigate: () => () => undefined,
|
||||
useParams: () => ({}),
|
||||
}))
|
||||
mock.module("@opencode-ai/ui/context", () => ({
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("encodeFilePath", () => {
|
||||
const url = new URL(fileUrl)
|
||||
expect(url.protocol).toBe("file:")
|
||||
expect(url.pathname).toContain("README.bs.md")
|
||||
expect(result).toBe("/D%3A/dev/projects/opencode/README.bs.md")
|
||||
expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
|
||||
})
|
||||
|
||||
test("should handle mixed separator path (Windows + Unix)", () => {
|
||||
@@ -118,7 +118,7 @@ describe("encodeFilePath", () => {
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
expect(result).toBe("/D%3A/dev/projects/opencode/README.bs.md")
|
||||
expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
|
||||
})
|
||||
|
||||
test("should handle Windows path with spaces", () => {
|
||||
@@ -146,7 +146,7 @@ describe("encodeFilePath", () => {
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
expect(result).toBe("/C%3A/")
|
||||
expect(result).toBe("/C:/")
|
||||
})
|
||||
|
||||
test("should handle Windows relative path with backslashes", () => {
|
||||
@@ -177,7 +177,7 @@ describe("encodeFilePath", () => {
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
expect(result).toBe("/c%3A/users/test/file.txt")
|
||||
expect(result).toBe("/c:/users/test/file.txt")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("encodeFilePath", () => {
|
||||
const result = encodeFilePath(windowsPath)
|
||||
// Should convert to forward slashes and add leading /
|
||||
expect(result).not.toContain("\\")
|
||||
expect(result).toMatch(/^\/[A-Za-z]%3A\//)
|
||||
expect(result).toMatch(/^\/[A-Za-z]:\//)
|
||||
})
|
||||
|
||||
test("should handle relative paths the same on all platforms", () => {
|
||||
@@ -237,7 +237,7 @@ describe("encodeFilePath", () => {
|
||||
const result = encodeFilePath(alreadyNormalized)
|
||||
|
||||
// Should not add another leading slash
|
||||
expect(result).toBe("/D%3A/path/file.txt")
|
||||
expect(result).toBe("/D:/path/file.txt")
|
||||
expect(result).not.toContain("//D")
|
||||
})
|
||||
|
||||
@@ -246,7 +246,7 @@ describe("encodeFilePath", () => {
|
||||
const result = encodeFilePath(justDrive)
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(result).toBe("/D%3A")
|
||||
expect(result).toBe("/D:")
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
})
|
||||
|
||||
@@ -256,7 +256,7 @@ describe("encodeFilePath", () => {
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
expect(result).toBe("/C%3A/Users/test/")
|
||||
expect(result).toBe("/C:/Users/test/")
|
||||
})
|
||||
|
||||
test("should handle very long paths", () => {
|
||||
|
||||
@@ -90,9 +90,14 @@ export function encodeFilePath(filepath: string): string {
|
||||
}
|
||||
|
||||
// Encode each path segment (preserving forward slashes as path separators)
|
||||
// Keep the colon in Windows drive letters (`/C:/...`) so downstream file URL parsers
|
||||
// can reliably detect drives.
|
||||
return normalized
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.map((segment, index) => {
|
||||
if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment
|
||||
return encodeURIComponent(segment)
|
||||
})
|
||||
.join("/")
|
||||
}
|
||||
|
||||
|
||||
@@ -231,6 +231,24 @@ export function applyDirectoryEvent(input: {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "message.part.delta": {
|
||||
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
|
||||
const parts = input.store.part[props.messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
||||
if (!result.found) break
|
||||
input.setStore(
|
||||
"part",
|
||||
props.messageID,
|
||||
produce((draft) => {
|
||||
const part = draft[result.index]
|
||||
const field = props.field as keyof typeof part
|
||||
const existing = part[field] as string | undefined
|
||||
;(part[field] as string) = (existing ?? "") + props.delta
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "vcs.branch.updated": {
|
||||
const props = event.properties as { branch: string }
|
||||
const next = { branch: props.branch }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSync } from "./sync"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useModels } from "@/context/models"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
|
||||
export type ModelKey = { providerID: string; modelID: string }
|
||||
|
||||
@@ -184,11 +185,27 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
models.setVisibility(model, visible)
|
||||
},
|
||||
variant: {
|
||||
current() {
|
||||
configured() {
|
||||
const a = agent.current()
|
||||
const m = current()
|
||||
if (!a || !m) return undefined
|
||||
return getConfiguredAgentVariant({
|
||||
agent: { model: a.model, variant: a.variant },
|
||||
model: { providerID: m.provider.id, modelID: m.id, variants: m.variants },
|
||||
})
|
||||
},
|
||||
selected() {
|
||||
const m = current()
|
||||
if (!m) return undefined
|
||||
return models.variant.get({ providerID: m.provider.id, modelID: m.id })
|
||||
},
|
||||
current() {
|
||||
return resolveModelVariant({
|
||||
variants: this.list(),
|
||||
selected: this.selected(),
|
||||
configured: this.configured(),
|
||||
})
|
||||
},
|
||||
list() {
|
||||
const m = current()
|
||||
if (!m) return []
|
||||
@@ -203,17 +220,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
cycle() {
|
||||
const variants = this.list()
|
||||
if (variants.length === 0) return
|
||||
const currentVariant = this.current()
|
||||
if (!currentVariant) {
|
||||
this.set(variants[0])
|
||||
return
|
||||
}
|
||||
const index = variants.indexOf(currentVariant)
|
||||
if (index === -1 || index === variants.length - 1) {
|
||||
this.set(undefined)
|
||||
return
|
||||
}
|
||||
this.set(variants[index + 1])
|
||||
this.set(
|
||||
cycleModelVariant({
|
||||
variants,
|
||||
selected: this.selected(),
|
||||
configured: this.configured(),
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
66
packages/app/src/context/model-variant.test.ts
Normal file
66
packages/app/src/context/model-variant.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
|
||||
describe("model variant", () => {
|
||||
test("resolves configured agent variant when model matches", () => {
|
||||
const value = getConfiguredAgentVariant({
|
||||
agent: {
|
||||
model: { providerID: "openai", modelID: "gpt-5.2" },
|
||||
variant: "xhigh",
|
||||
},
|
||||
model: {
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.2",
|
||||
variants: { low: {}, high: {}, xhigh: {} },
|
||||
},
|
||||
})
|
||||
|
||||
expect(value).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("ignores configured variant when model does not match", () => {
|
||||
const value = getConfiguredAgentVariant({
|
||||
agent: {
|
||||
model: { providerID: "openai", modelID: "gpt-5.2" },
|
||||
variant: "xhigh",
|
||||
},
|
||||
model: {
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-sonnet-4",
|
||||
variants: { low: {}, high: {}, xhigh: {} },
|
||||
},
|
||||
})
|
||||
|
||||
expect(value).toBeUndefined()
|
||||
})
|
||||
|
||||
test("prefers selected variant over configured variant", () => {
|
||||
const value = resolveModelVariant({
|
||||
variants: ["low", "high", "xhigh"],
|
||||
selected: "high",
|
||||
configured: "xhigh",
|
||||
})
|
||||
|
||||
expect(value).toBe("high")
|
||||
})
|
||||
|
||||
test("cycles from configured variant to next", () => {
|
||||
const value = cycleModelVariant({
|
||||
variants: ["low", "high", "xhigh"],
|
||||
selected: undefined,
|
||||
configured: "high",
|
||||
})
|
||||
|
||||
expect(value).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("wraps from configured last variant to first", () => {
|
||||
const value = cycleModelVariant({
|
||||
variants: ["low", "high", "xhigh"],
|
||||
selected: undefined,
|
||||
configured: "xhigh",
|
||||
})
|
||||
|
||||
expect(value).toBe("low")
|
||||
})
|
||||
})
|
||||
50
packages/app/src/context/model-variant.ts
Normal file
50
packages/app/src/context/model-variant.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
type AgentModel = {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
|
||||
type Agent = {
|
||||
model?: AgentModel
|
||||
variant?: string
|
||||
}
|
||||
|
||||
type Model = AgentModel & {
|
||||
variants?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type VariantInput = {
|
||||
variants: string[]
|
||||
selected: string | undefined
|
||||
configured: string | undefined
|
||||
}
|
||||
|
||||
export function getConfiguredAgentVariant(input: { agent: Agent | undefined; model: Model | undefined }) {
|
||||
if (!input.agent?.variant) return undefined
|
||||
if (!input.agent.model) return undefined
|
||||
if (!input.model?.variants) return undefined
|
||||
if (input.agent.model.providerID !== input.model.providerID) return undefined
|
||||
if (input.agent.model.modelID !== input.model.modelID) return undefined
|
||||
if (!(input.agent.variant in input.model.variants)) return undefined
|
||||
return input.agent.variant
|
||||
}
|
||||
|
||||
export function resolveModelVariant(input: VariantInput) {
|
||||
if (input.selected && input.variants.includes(input.selected)) return input.selected
|
||||
if (input.configured && input.variants.includes(input.configured)) return input.configured
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function cycleModelVariant(input: VariantInput) {
|
||||
if (input.variants.length === 0) return undefined
|
||||
if (input.selected && input.variants.includes(input.selected)) {
|
||||
const index = input.variants.indexOf(input.selected)
|
||||
if (index === input.variants.length - 1) return undefined
|
||||
return input.variants[index + 1]
|
||||
}
|
||||
if (input.configured && input.variants.includes(input.configured)) {
|
||||
const index = input.variants.indexOf(input.configured)
|
||||
if (index === input.variants.length - 1) return input.variants[0]
|
||||
return input.variants[index + 1]
|
||||
}
|
||||
return input.variants[0]
|
||||
}
|
||||
@@ -5,6 +5,7 @@ let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => str
|
||||
|
||||
beforeAll(async () => {
|
||||
mock.module("@solidjs/router", () => ({
|
||||
useNavigate: () => () => undefined,
|
||||
useParams: () => ({}),
|
||||
}))
|
||||
mock.module("@opencode-ai/ui/context", () => ({
|
||||
|
||||
@@ -13,7 +13,7 @@ export type LocalPTY = {
|
||||
cols?: number
|
||||
buffer?: string
|
||||
scrollY?: number
|
||||
tail?: string
|
||||
cursor?: number
|
||||
}
|
||||
|
||||
const WORKSPACE_KEY = "__workspace__"
|
||||
|
||||
@@ -208,8 +208,8 @@ export const dict = {
|
||||
"model.tooltip.context": "Context limit {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Search",
|
||||
"common.goBack": "Back",
|
||||
"common.goForward": "Forward",
|
||||
"common.goBack": "Navigate back",
|
||||
"common.goForward": "Navigate forward",
|
||||
"common.loading": "Loading",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Cancel",
|
||||
|
||||
@@ -25,7 +25,8 @@ export default function Home() {
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
const recent = createMemo(() => {
|
||||
return sync.data.project
|
||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice()
|
||||
.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice(0, 5)
|
||||
})
|
||||
|
||||
|
||||
@@ -1272,8 +1272,6 @@ export default function Layout(props: ParentProps) {
|
||||
),
|
||||
)
|
||||
|
||||
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
||||
|
||||
setBusy(directory, false)
|
||||
dismiss()
|
||||
|
||||
@@ -1938,7 +1936,7 @@ export default function Layout(props: ParentProps) {
|
||||
direction="horizontal"
|
||||
size={layout.sidebar.width()}
|
||||
min={244}
|
||||
max={window.innerWidth * 0.3 + 64}
|
||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
||||
collapseThreshold={244}
|
||||
onResize={layout.sidebar.resize}
|
||||
onCollapse={layout.sidebar.close}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const isRootVisibleSession = (session: Session, directory: string) =>
|
||||
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
|
||||
|
||||
export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) =>
|
||||
store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).toSorted(sortSessions(now))
|
||||
store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now))
|
||||
|
||||
export const childMapByParent = (sessions: Session[]) => {
|
||||
const map = new Map<string, string[]>()
|
||||
|
||||
@@ -144,7 +144,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
|
||||
const item = (
|
||||
<A
|
||||
href={`${props.slug}/session/${props.session.id}`}
|
||||
href={`/${props.slug}/session/${props.session.id}`}
|
||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
onPointerEnter={scheduleHoverPrefetch}
|
||||
onPointerLeave={cancelHoverPrefetch}
|
||||
@@ -285,7 +285,7 @@ export const NewSessionItem = (props: {
|
||||
const tooltip = () => props.mobile || !props.sidebarExpanded()
|
||||
const item = (
|
||||
<A
|
||||
href={`${props.slug}/session`}
|
||||
href={`/${props.slug}/session`}
|
||||
end
|
||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
onClick={() => {
|
||||
|
||||
@@ -118,7 +118,7 @@ export const SortableWorkspace = (props: {
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
const showNew = createMemo(() => !loading() && (touch() || sessions().length === 0 || (active() && !params.id)))
|
||||
const loadMore = async () => {
|
||||
setWorkspaceStore("limit", (limit) => limit + 5)
|
||||
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
await globalSync.project.loadSessions(props.directory)
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ export const LocalWorkspace = (props: {
|
||||
const loading = createMemo(() => !booted() && sessions().length === 0)
|
||||
const hasMore = createMemo(() => workspace().store.sessionTotal > sessions().length)
|
||||
const loadMore = async () => {
|
||||
workspace().setStore("limit", (limit) => limit + 5)
|
||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
await globalSync.project.loadSessions(props.project.worktree)
|
||||
}
|
||||
|
||||
|
||||
@@ -591,7 +591,7 @@ export default function Page() {
|
||||
const newSessionWorktree = createMemo(() => {
|
||||
if (store.newSessionWorktree === "create") return "create"
|
||||
const project = sync.project
|
||||
if (project && sync.data.path.directory !== project.worktree) return sync.data.path.directory
|
||||
if (project && sdk.directory !== project.worktree) return sdk.directory
|
||||
return "main"
|
||||
})
|
||||
|
||||
@@ -1026,10 +1026,31 @@ export default function Page() {
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class={input.emptyClass}>
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div>
|
||||
</div>
|
||||
<SessionReviewTab
|
||||
title={changesTitle()}
|
||||
empty={
|
||||
store.changes === "turn" ? (
|
||||
emptyTurn()
|
||||
) : (
|
||||
<div class={input.emptyClass}>
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
diffs={reviewDiffs}
|
||||
view={view}
|
||||
diffStyle={input.diffStyle}
|
||||
onDiffStyleChange={input.onDiffStyleChange}
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
focusedFile={tree.activeDiff}
|
||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||
comments={comments.all()}
|
||||
focusedComment={comments.focus()}
|
||||
onFocusedCommentChange={comments.setFocus}
|
||||
onViewFile={openReviewFile}
|
||||
classes={input.classes}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
@@ -1041,7 +1062,7 @@ export default function Page() {
|
||||
diffStyle: layout.review.diffStyle(),
|
||||
onDiffStyleChange: layout.review.setDiffStyle,
|
||||
loadingClass: "px-6 py-4 text-text-weak",
|
||||
emptyClass: "h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
emptyClass: "h-full pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1569,7 +1590,7 @@ export default function Page() {
|
||||
container: "px-4",
|
||||
},
|
||||
loadingClass: "px-4 py-4 text-text-weak",
|
||||
emptyClass: "h-full px-4 pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
emptyClass: "h-full pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
})}
|
||||
scroll={ui.scroll}
|
||||
onResumeScroll={resumeScroll}
|
||||
@@ -1647,7 +1668,7 @@ export default function Page() {
|
||||
|
||||
const target = value === "main" ? sync.project?.worktree : value
|
||||
if (!target) return
|
||||
if (target === sync.data.path.directory) return
|
||||
if (target === sdk.directory) return
|
||||
layout.projects.open(target)
|
||||
navigate(`/${base64Encode(target)}/session`)
|
||||
}}
|
||||
@@ -1683,7 +1704,7 @@ export default function Page() {
|
||||
direction="horizontal"
|
||||
size={layout.session.width()}
|
||||
min={450}
|
||||
max={window.innerWidth * 0.45}
|
||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45}
|
||||
onResize={layout.session.resize}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
@@ -179,7 +179,7 @@ export function MessageTimeline(props: {
|
||||
"sticky top-0 z-30 bg-background-stronger": true,
|
||||
"w-full": true,
|
||||
"px-4 md:px-6": true,
|
||||
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<div class="h-10 w-full flex items-center justify-between gap-2">
|
||||
@@ -278,7 +278,7 @@ export function MessageTimeline(props: {
|
||||
class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
||||
classList={{
|
||||
"w-full": true,
|
||||
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
"mt-0.5": props.centered,
|
||||
"mt-0": !props.centered,
|
||||
}}
|
||||
@@ -321,7 +321,7 @@ export function MessageTimeline(props: {
|
||||
}}
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-200 3xl:max-w-[1200px]": props.centered,
|
||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<SessionTurn
|
||||
|
||||
@@ -228,6 +228,7 @@ export const createScrollSpy = (input: Input) => {
|
||||
node.delete(key)
|
||||
visible.delete(key)
|
||||
dirty = true
|
||||
schedule()
|
||||
}
|
||||
|
||||
const markDirty = () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ export function SessionPromptDock(props: {
|
||||
<div
|
||||
classList={{
|
||||
"w-full px-4 pointer-events-auto": true,
|
||||
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<Show when={props.questionRequest()} keyed>
|
||||
|
||||
@@ -41,7 +41,7 @@ export function TerminalPanel(props: {
|
||||
direction="vertical"
|
||||
size={props.height}
|
||||
min={100}
|
||||
max={window.innerHeight * 0.6}
|
||||
max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6}
|
||||
collapseThreshold={50}
|
||||
onResize={props.resize}
|
||||
onCollapse={props.close}
|
||||
|
||||
@@ -365,48 +365,81 @@ export const useSessionCommands = (input: {
|
||||
return [
|
||||
{
|
||||
id: "session.share",
|
||||
title: input.info()?.share?.url ? "Copy share link" : input.language.t("command.session.share"),
|
||||
title: input.info()?.share?.url
|
||||
? input.language.t("session.share.copy.copyLink")
|
||||
: input.language.t("command.session.share"),
|
||||
description: input.info()?.share?.url
|
||||
? "Copy share URL to clipboard"
|
||||
? input.language.t("toast.session.share.success.description")
|
||||
: input.language.t("command.session.share.description"),
|
||||
category: input.language.t("command.category.session"),
|
||||
slash: "share",
|
||||
disabled: !input.params.id,
|
||||
onSelect: async () => {
|
||||
if (!input.params.id) return
|
||||
const copy = (url: string, existing: boolean) =>
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: existing
|
||||
? input.language.t("session.share.copy.copied")
|
||||
: input.language.t("toast.session.share.success.title"),
|
||||
description: input.language.t("toast.session.share.success.description"),
|
||||
variant: "success",
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: input.language.t("toast.session.share.copyFailed.title"),
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
const url = input.info()?.share?.url
|
||||
if (url) {
|
||||
await copy(url, true)
|
||||
|
||||
const write = (value: string) => {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
if (body) {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = value
|
||||
textarea.setAttribute("readonly", "")
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.opacity = "0"
|
||||
textarea.style.pointerEvents = "none"
|
||||
body.appendChild(textarea)
|
||||
textarea.select()
|
||||
const copied = document.execCommand("copy")
|
||||
body.removeChild(textarea)
|
||||
if (copied) return Promise.resolve(true)
|
||||
}
|
||||
|
||||
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
||||
if (!clipboard?.writeText) return Promise.resolve(false)
|
||||
return clipboard.writeText(value).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
}
|
||||
|
||||
const copy = async (url: string, existing: boolean) => {
|
||||
const ok = await write(url)
|
||||
if (!ok) {
|
||||
showToast({
|
||||
title: input.language.t("toast.session.share.copyFailed.title"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
showToast({
|
||||
title: existing
|
||||
? input.language.t("session.share.copy.copied")
|
||||
: input.language.t("toast.session.share.success.title"),
|
||||
description: input.language.t("toast.session.share.success.description"),
|
||||
variant: "success",
|
||||
})
|
||||
}
|
||||
|
||||
const existing = input.info()?.share?.url
|
||||
if (existing) {
|
||||
await copy(existing, true)
|
||||
return
|
||||
}
|
||||
await input.sdk.client.session
|
||||
|
||||
const url = await input.sdk.client.session
|
||||
.share({ sessionID: input.params.id })
|
||||
.then((res) => copy(res.data!.share!.url, false))
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: input.language.t("toast.session.share.failed.title"),
|
||||
description: input.language.t("toast.session.share.failed.description"),
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
.then((res) => res.data?.share?.url)
|
||||
.catch(() => undefined)
|
||||
if (!url) {
|
||||
showToast({
|
||||
title: input.language.t("toast.session.share.failed.title"),
|
||||
description: input.language.t("toast.session.share.failed.description"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await copy(url, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -99,4 +99,9 @@ describe("persist localStorage resilience", () => {
|
||||
|
||||
expect(storage.getItem("direct-value")).toBe('{"value":5}')
|
||||
})
|
||||
|
||||
test("normalizer rejects malformed JSON payloads", () => {
|
||||
const result = persistTesting.normalize({ value: "ok" }, '{"value":"\\x"}')
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -195,6 +195,14 @@ function parse(value: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalize(defaults: unknown, raw: string, migrate?: (value: unknown) => unknown) {
|
||||
const parsed = parse(raw)
|
||||
if (parsed === undefined) return
|
||||
const migrated = migrate ? migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
return JSON.stringify(merged)
|
||||
}
|
||||
|
||||
function workspaceStorage(dir: string) {
|
||||
const head = dir.slice(0, 12) || "workspace"
|
||||
const sum = checksum(dir) ?? "0"
|
||||
@@ -291,6 +299,7 @@ function localStorageDirect(): SyncStorage {
|
||||
export const PersistTesting = {
|
||||
localStorageDirect,
|
||||
localStorageWithPrefix,
|
||||
normalize,
|
||||
}
|
||||
|
||||
export const Persist = {
|
||||
@@ -358,12 +367,11 @@ export function persisted<T>(
|
||||
getItem: (key) => {
|
||||
const raw = current.getItem(key)
|
||||
if (raw !== null) {
|
||||
const parsed = parse(raw)
|
||||
if (parsed === undefined) return raw
|
||||
|
||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
const next = JSON.stringify(merged)
|
||||
const next = normalize(defaults, raw, config.migrate)
|
||||
if (next === undefined) {
|
||||
current.removeItem(key)
|
||||
return null
|
||||
}
|
||||
if (raw !== next) current.setItem(key, next)
|
||||
return next
|
||||
}
|
||||
@@ -372,16 +380,13 @@ export function persisted<T>(
|
||||
const legacyRaw = legacyStore.getItem(legacyKey)
|
||||
if (legacyRaw === null) continue
|
||||
|
||||
current.setItem(key, legacyRaw)
|
||||
const next = normalize(defaults, legacyRaw, config.migrate)
|
||||
if (next === undefined) {
|
||||
legacyStore.removeItem(legacyKey)
|
||||
continue
|
||||
}
|
||||
current.setItem(key, next)
|
||||
legacyStore.removeItem(legacyKey)
|
||||
|
||||
const parsed = parse(legacyRaw)
|
||||
if (parsed === undefined) return legacyRaw
|
||||
|
||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
const next = JSON.stringify(merged)
|
||||
if (legacyRaw !== next) current.setItem(key, next)
|
||||
return next
|
||||
}
|
||||
|
||||
@@ -405,12 +410,11 @@ export function persisted<T>(
|
||||
getItem: async (key) => {
|
||||
const raw = await current.getItem(key)
|
||||
if (raw !== null) {
|
||||
const parsed = parse(raw)
|
||||
if (parsed === undefined) return raw
|
||||
|
||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
const next = JSON.stringify(merged)
|
||||
const next = normalize(defaults, raw, config.migrate)
|
||||
if (next === undefined) {
|
||||
await current.removeItem(key).catch(() => undefined)
|
||||
return null
|
||||
}
|
||||
if (raw !== next) await current.setItem(key, next)
|
||||
return next
|
||||
}
|
||||
@@ -421,16 +425,13 @@ export function persisted<T>(
|
||||
const legacyRaw = await legacyStore.getItem(legacyKey)
|
||||
if (legacyRaw === null) continue
|
||||
|
||||
await current.setItem(key, legacyRaw)
|
||||
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)
|
||||
|
||||
const parsed = parse(legacyRaw)
|
||||
if (parsed === undefined) return legacyRaw
|
||||
|
||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
const next = JSON.stringify(merged)
|
||||
if (legacyRaw !== next) await current.setItem(key, next)
|
||||
return next
|
||||
}
|
||||
|
||||
|
||||
@@ -351,8 +351,8 @@ export const dict = {
|
||||
"changelog.empty":
|
||||
"\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0623\u064a \u0625\u062f\u062e\u0627\u0644\u0627\u062a \u0641\u064a \u0633\u062c\u0644 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a.",
|
||||
"changelog.viewJson": "\u0639\u0631\u0636 JSON",
|
||||
"workspace.nav.zen": "زين",
|
||||
"workspace.nav.apiKeys": "API المفاتيح",
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.apiKeys": "مفاتيح API",
|
||||
"workspace.nav.members": "أعضاء",
|
||||
"workspace.nav.billing": "الفواتير",
|
||||
"workspace.nav.settings": "إعدادات",
|
||||
@@ -365,14 +365,14 @@ export const dict = {
|
||||
"workspace.newUser.feature.quality.title": "أعلى جودة",
|
||||
"workspace.newUser.feature.quality.body":
|
||||
"الوصول إلى النماذج التي تم تكوينها لتحقيق الأداء الأمثل - لا يوجد تخفيضات أو توجيه إلى موفري الخدمة الأرخص.",
|
||||
"workspace.newUser.feature.lockin.title": "لا يوجد قفل",
|
||||
"workspace.newUser.feature.lockin.title": "بدون احتجاز بمزوّد واحد",
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"استخدم Zen مع أي وكيل ترميز، واستمر في استخدام موفري الخدمات الآخرين مع opencode وقتما تشاء.",
|
||||
"workspace.newUser.copyApiKey": "انسخ مفتاح API",
|
||||
"workspace.newUser.copyKey": "نسخ المفتاح",
|
||||
"workspace.newUser.copied": "منسوخ!",
|
||||
"workspace.newUser.step.enableBilling": "تمكين الفوترة",
|
||||
"workspace.newUser.step.login.before": "يجري",
|
||||
"workspace.newUser.step.login.before": "شغّل",
|
||||
"workspace.newUser.step.login.after": "وحدد opencode",
|
||||
"workspace.newUser.step.pasteKey": "الصق مفتاح API الخاص بك",
|
||||
"workspace.newUser.step.models.before": "ابدأ opencode ثم قم بالتشغيل",
|
||||
@@ -390,7 +390,7 @@ export const dict = {
|
||||
"workspace.providers.saving": "توفير...",
|
||||
"workspace.providers.save": "يحفظ",
|
||||
"workspace.providers.table.provider": "مزود",
|
||||
"workspace.providers.table.apiKey": "API المفتاح",
|
||||
"workspace.providers.table.apiKey": "مفتاح API",
|
||||
"workspace.usage.title": "تاريخ الاستخدام",
|
||||
"workspace.usage.subtitle": "استخدام وتكاليف API الأخيرة.",
|
||||
"workspace.usage.empty": "قم بإجراء أول مكالمة API للبدء.",
|
||||
@@ -398,25 +398,25 @@ export const dict = {
|
||||
"workspace.usage.table.model": "نموذج",
|
||||
"workspace.usage.table.input": "مدخل",
|
||||
"workspace.usage.table.output": "الإخراج",
|
||||
"workspace.usage.table.cost": "يكلف",
|
||||
"workspace.usage.table.cost": "التكلفة",
|
||||
"workspace.usage.breakdown.input": "مدخل",
|
||||
"workspace.usage.breakdown.cacheRead": "قراءة ذاكرة التخزين المؤقت",
|
||||
"workspace.usage.breakdown.cacheWrite": "كتابة ذاكرة التخزين المؤقت",
|
||||
"workspace.usage.breakdown.output": "الإخراج",
|
||||
"workspace.usage.breakdown.reasoning": "المنطق",
|
||||
"workspace.usage.subscription": "الاشتراك (${{amount}})",
|
||||
"workspace.cost.title": "يكلف",
|
||||
"workspace.cost.title": "التكلفة",
|
||||
"workspace.cost.subtitle": "تكاليف الاستخدام مقسمة حسب النموذج.",
|
||||
"workspace.cost.allModels": "جميع الموديلات",
|
||||
"workspace.cost.allModels": "جميع النماذج",
|
||||
"workspace.cost.allKeys": "جميع المفاتيح",
|
||||
"workspace.cost.deletedSuffix": "(محذوف)",
|
||||
"workspace.cost.empty": "لا توجد بيانات استخدام متاحة للفترة المحددة.",
|
||||
"workspace.cost.subscriptionShort": "الفرعية",
|
||||
"workspace.keys.title": "API المفاتيح",
|
||||
"workspace.cost.subscriptionShort": "اشتراك",
|
||||
"workspace.keys.title": "مفاتيح API",
|
||||
"workspace.keys.subtitle": "إدارة مفاتيح API الخاصة بك للوصول إلى خدمات opencode.",
|
||||
"workspace.keys.create": "قم بإنشاء مفتاح API",
|
||||
"workspace.keys.placeholder": "أدخل اسم المفتاح",
|
||||
"workspace.keys.empty": "قم بإنشاء مفتاح opencode للبوابة API",
|
||||
"workspace.keys.empty": "أنشئ مفتاح API لبوابة opencode",
|
||||
"workspace.keys.table.name": "اسم",
|
||||
"workspace.keys.table.key": "مفتاح",
|
||||
"workspace.keys.table.createdBy": "تم الإنشاء بواسطة",
|
||||
@@ -442,14 +442,14 @@ export const dict = {
|
||||
"workspace.members.table.email": "بريد إلكتروني",
|
||||
"workspace.members.table.role": "دور",
|
||||
"workspace.members.table.monthLimit": "حد الشهر",
|
||||
"workspace.members.role.admin": "مسؤل",
|
||||
"workspace.members.role.admin": "مسؤول",
|
||||
"workspace.members.role.adminDescription": "يمكن إدارة النماذج، والأعضاء، والفواتير",
|
||||
"workspace.members.role.member": "عضو",
|
||||
"workspace.members.role.memberDescription": "يمكنهم فقط إنشاء مفاتيح API لأنفسهم",
|
||||
"workspace.settings.title": "إعدادات",
|
||||
"workspace.settings.subtitle": "قم بتحديث اسم مساحة العمل الخاصة بك وتفضيلاتك.",
|
||||
"workspace.settings.workspaceName": "اسم مساحة العمل",
|
||||
"workspace.settings.defaultName": "تقصير",
|
||||
"workspace.settings.defaultName": "الافتراضي",
|
||||
"workspace.settings.updating": "جارٍ التحديث...",
|
||||
"workspace.settings.save": "يحفظ",
|
||||
"workspace.settings.edit": "يحرر",
|
||||
@@ -461,37 +461,37 @@ export const dict = {
|
||||
"workspace.billing.add": "أضف $",
|
||||
"workspace.billing.enterAmount": "أدخل المبلغ",
|
||||
"workspace.billing.loading": "تحميل...",
|
||||
"workspace.billing.addAction": "يضيف",
|
||||
"workspace.billing.addAction": "إضافة",
|
||||
"workspace.billing.addBalance": "إضافة الرصيد",
|
||||
"workspace.billing.linkedToStripe": "مرتبطة بالشريط",
|
||||
"workspace.billing.manage": "يدير",
|
||||
"workspace.billing.linkedToStripe": "مرتبط بـ Stripe",
|
||||
"workspace.billing.manage": "إدارة",
|
||||
"workspace.billing.enable": "تمكين الفوترة",
|
||||
"workspace.monthlyLimit.title": "الحد الشهري",
|
||||
"workspace.monthlyLimit.subtitle": "قم بتعيين حد الاستخدام الشهري لحسابك.",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "جلسة...",
|
||||
"workspace.monthlyLimit.setting": "جارٍ التعيين...",
|
||||
"workspace.monthlyLimit.set": "تعيين",
|
||||
"workspace.monthlyLimit.edit": "تحرير الحد",
|
||||
"workspace.monthlyLimit.noLimit": "لم يتم تعيين حد الاستخدام.",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي ل",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "هو $",
|
||||
"workspace.reload.title": "إعادة التحميل التلقائي",
|
||||
"workspace.reload.disabled.before": "إعادة التحميل التلقائي هو",
|
||||
"workspace.reload.disabled.state": "عاجز",
|
||||
"workspace.reload.disabled.after": "تمكين إعادة التحميل تلقائيًا عندما يكون الرصيد منخفضًا.",
|
||||
"workspace.reload.enabled.before": "إعادة التحميل التلقائي هو",
|
||||
"workspace.reload.title": "إعادة الشحن التلقائي",
|
||||
"workspace.reload.disabled.before": "إعادة الشحن التلقائي",
|
||||
"workspace.reload.disabled.state": "معطّل",
|
||||
"workspace.reload.disabled.after": "فعّلها لإعادة شحن الرصيد تلقائيًا عندما يكون منخفضًا.",
|
||||
"workspace.reload.enabled.before": "إعادة الشحن التلقائي",
|
||||
"workspace.reload.enabled.state": "ممكّن",
|
||||
"workspace.reload.enabled.middle": "سنقوم بإعادة التحميل",
|
||||
"workspace.reload.enabled.middle": "سنعيد شحن رصيدك بمبلغ",
|
||||
"workspace.reload.processingFee": "رسوم المعالجة",
|
||||
"workspace.reload.enabled.after": "عندما يصل التوازن",
|
||||
"workspace.reload.enabled.after": "عندما يصل الرصيد إلى",
|
||||
"workspace.reload.edit": "يحرر",
|
||||
"workspace.reload.enable": "يُمكَِن",
|
||||
"workspace.reload.enableAutoReload": "تمكين إعادة التحميل التلقائي",
|
||||
"workspace.reload.reloadAmount": "إعادة تحميل $",
|
||||
"workspace.reload.enable": "تفعيل",
|
||||
"workspace.reload.enableAutoReload": "تفعيل إعادة الشحن التلقائي",
|
||||
"workspace.reload.reloadAmount": "مبلغ إعادة الشحن $",
|
||||
"workspace.reload.whenBalanceReaches": "عندما يصل الرصيد إلى $",
|
||||
"workspace.reload.saving": "توفير...",
|
||||
"workspace.reload.save": "يحفظ",
|
||||
"workspace.reload.failedAt": "فشلت عملية إعادة التحميل عند",
|
||||
"workspace.reload.failedAt": "فشلت إعادة الشحن في",
|
||||
"workspace.reload.reason": "سبب:",
|
||||
"workspace.reload.updatePaymentMethod": "يرجى تحديث طريقة الدفع الخاصة بك والمحاولة مرة أخرى.",
|
||||
"workspace.reload.retrying": "جارٍ إعادة المحاولة...",
|
||||
@@ -500,11 +500,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "معاملات الدفع الأخيرة.",
|
||||
"workspace.payments.table.date": "تاريخ",
|
||||
"workspace.payments.table.paymentId": "معرف الدفع",
|
||||
"workspace.payments.table.amount": "كمية",
|
||||
"workspace.payments.table.amount": "المبلغ",
|
||||
"workspace.payments.table.receipt": "إيصال",
|
||||
"workspace.payments.type.credit": "ائتمان",
|
||||
"workspace.payments.type.subscription": "الاشتراك",
|
||||
"workspace.payments.view": "منظر",
|
||||
"workspace.payments.view": "عرض",
|
||||
"workspace.black.loading": "تحميل...",
|
||||
"workspace.black.time.day": "يوم",
|
||||
"workspace.black.time.days": "أيام",
|
||||
@@ -521,8 +521,8 @@ export const dict = {
|
||||
"workspace.black.subscription.resetsIn": "إعادة تعيين في",
|
||||
"workspace.black.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
|
||||
"workspace.black.waitlist.title": "قائمة الانتظار",
|
||||
"workspace.black.waitlist.joined": "أنت على قائمة الانتظار للخطة السوداء {{plan}} دولار شهريًا OpenCode.",
|
||||
"workspace.black.waitlist.ready": "نحن على استعداد لتسجيلك في خطة Black {{plan}} الشهرية OpenCode.",
|
||||
"workspace.black.waitlist.joined": "أنت على قائمة الانتظار لخطة OpenCode Black بقيمة ${{plan}} شهريًا.",
|
||||
"workspace.black.waitlist.ready": "نحن مستعدون لتسجيلك في خطة OpenCode Black بقيمة ${{plan}} شهريًا.",
|
||||
"workspace.black.waitlist.leave": "ترك قائمة الانتظار",
|
||||
"workspace.black.waitlist.leaving": "مغادرة...",
|
||||
"workspace.black.waitlist.left": "غادر",
|
||||
|
||||
@@ -294,18 +294,18 @@ export const dict = {
|
||||
"workspace.home.billing.currentBalance": "Nuværende saldo",
|
||||
"workspace.newUser.feature.tested.title": "Testede og verificerede modeller",
|
||||
"workspace.newUser.feature.tested.body":
|
||||
"Vi har benchmarket og testet modeller specifikt til kodningsmidler for at sikre den bedste ydeevne.",
|
||||
"Vi har benchmarket og testet modeller specifikt til kodningsagenter for at sikre den bedste ydeevne.",
|
||||
"workspace.newUser.feature.quality.title": "Højeste kvalitet",
|
||||
"workspace.newUser.feature.quality.body":
|
||||
"Få adgang til modeller konfigureret til optimal ydeevne - ingen nedgraderinger eller routing til billigere udbydere.",
|
||||
"workspace.newUser.feature.lockin.title": "Ingen indlåsning",
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"Brug Zen med en hvilken som helst kodningsagent, og fortsæt med at bruge andre udbydere med opencode, når du vil.",
|
||||
"workspace.newUser.copyApiKey": "Kopiér nøglen API",
|
||||
"workspace.newUser.copyApiKey": "Kopiér API-nøgle",
|
||||
"workspace.newUser.copyKey": "Kopier nøgle",
|
||||
"workspace.newUser.copied": "Kopieret!",
|
||||
"workspace.newUser.step.enableBilling": "Aktiver fakturering",
|
||||
"workspace.newUser.step.login.before": "Løbe",
|
||||
"workspace.newUser.step.login.before": "Kør",
|
||||
"workspace.newUser.step.login.after": "og vælg opencode",
|
||||
"workspace.newUser.step.pasteKey": "Indsæt din API nøgle",
|
||||
"workspace.newUser.step.models.before": "Start opencode og kør",
|
||||
@@ -316,12 +316,12 @@ export const dict = {
|
||||
"workspace.models.table.enabled": "Aktiveret",
|
||||
"workspace.providers.title": "Medbring din egen nøgle",
|
||||
"workspace.providers.subtitle": "Konfigurer dine egne API nøgler fra AI-udbydere.",
|
||||
"workspace.providers.placeholder": "Indtast nøglen {{provider}} API ({{prefix}}...)",
|
||||
"workspace.providers.placeholder": "Indtast {{provider}} API-nøgle ({{prefix}}...)",
|
||||
"workspace.providers.configure": "Konfigurer",
|
||||
"workspace.providers.edit": "Redigere",
|
||||
"workspace.providers.edit": "Rediger",
|
||||
"workspace.providers.delete": "Slet",
|
||||
"workspace.providers.saving": "Gemmer...",
|
||||
"workspace.providers.save": "Spare",
|
||||
"workspace.providers.save": "Gem",
|
||||
"workspace.providers.table.provider": "Udbyder",
|
||||
"workspace.providers.table.apiKey": "API Nøgle",
|
||||
"workspace.usage.title": "Brugshistorik",
|
||||
@@ -330,15 +330,15 @@ export const dict = {
|
||||
"workspace.usage.table.date": "Dato",
|
||||
"workspace.usage.table.model": "Model",
|
||||
"workspace.usage.table.input": "Input",
|
||||
"workspace.usage.table.output": "Produktion",
|
||||
"workspace.usage.table.cost": "Koste",
|
||||
"workspace.usage.table.output": "Output",
|
||||
"workspace.usage.table.cost": "Omkostning",
|
||||
"workspace.usage.breakdown.input": "Input",
|
||||
"workspace.usage.breakdown.cacheRead": "Cache læst",
|
||||
"workspace.usage.breakdown.cacheWrite": "Cache skriv",
|
||||
"workspace.usage.breakdown.output": "Produktion",
|
||||
"workspace.usage.breakdown.output": "Output",
|
||||
"workspace.usage.breakdown.reasoning": "Ræsonnement",
|
||||
"workspace.usage.subscription": "abonnement (${{amount}})",
|
||||
"workspace.cost.title": "Koste",
|
||||
"workspace.cost.title": "Omkostninger",
|
||||
"workspace.cost.subtitle": "Brugsomkostninger opdelt efter model.",
|
||||
"workspace.cost.allModels": "Alle modeller",
|
||||
"workspace.cost.allKeys": "Alle nøgler",
|
||||
@@ -354,7 +354,7 @@ export const dict = {
|
||||
"workspace.keys.table.key": "Nøgle",
|
||||
"workspace.keys.table.createdBy": "Skabt af",
|
||||
"workspace.keys.table.lastUsed": "Sidst brugt",
|
||||
"workspace.keys.copyApiKey": "Kopiér nøglen API",
|
||||
"workspace.keys.copyApiKey": "Kopiér API-nøgle",
|
||||
"workspace.keys.delete": "Slet",
|
||||
"workspace.members.title": "Medlemmer",
|
||||
"workspace.members.subtitle": "Administrer arbejdsområdemedlemmer og deres tilladelser.",
|
||||
@@ -368,10 +368,10 @@ export const dict = {
|
||||
"workspace.members.noLimit": "Ingen grænse",
|
||||
"workspace.members.noLimitLowercase": "ingen grænse",
|
||||
"workspace.members.invited": "inviteret",
|
||||
"workspace.members.edit": "Redigere",
|
||||
"workspace.members.edit": "Rediger",
|
||||
"workspace.members.delete": "Slet",
|
||||
"workspace.members.saving": "Gemmer...",
|
||||
"workspace.members.save": "Spare",
|
||||
"workspace.members.save": "Gem",
|
||||
"workspace.members.table.email": "E-mail",
|
||||
"workspace.members.table.role": "Rolle",
|
||||
"workspace.members.table.monthLimit": "Månedsgrænse",
|
||||
@@ -382,10 +382,10 @@ export const dict = {
|
||||
"workspace.settings.title": "Indstillinger",
|
||||
"workspace.settings.subtitle": "Opdater dit arbejdsområdes navn og præferencer.",
|
||||
"workspace.settings.workspaceName": "Arbejdsområdets navn",
|
||||
"workspace.settings.defaultName": "Misligholdelse",
|
||||
"workspace.settings.defaultName": "Standard",
|
||||
"workspace.settings.updating": "Opdaterer...",
|
||||
"workspace.settings.save": "Spare",
|
||||
"workspace.settings.edit": "Redigere",
|
||||
"workspace.settings.save": "Gem",
|
||||
"workspace.settings.edit": "Rediger",
|
||||
"workspace.billing.title": "Fakturering",
|
||||
"workspace.billing.subtitle.beforeLink": "Administrer betalingsmetoder.",
|
||||
"workspace.billing.contactUs": "Kontakt os",
|
||||
@@ -394,10 +394,10 @@ export const dict = {
|
||||
"workspace.billing.add": "Tilføj $",
|
||||
"workspace.billing.enterAmount": "Indtast beløb",
|
||||
"workspace.billing.loading": "Indlæser...",
|
||||
"workspace.billing.addAction": "Tilføje",
|
||||
"workspace.billing.addAction": "Tilføj",
|
||||
"workspace.billing.addBalance": "Tilføj balance",
|
||||
"workspace.billing.linkedToStripe": "Forbundet til Stripe",
|
||||
"workspace.billing.manage": "Styre",
|
||||
"workspace.billing.manage": "Administrer",
|
||||
"workspace.billing.enable": "Aktiver fakturering",
|
||||
"workspace.monthlyLimit.title": "Månedlig grænse",
|
||||
"workspace.monthlyLimit.subtitle": "Indstil en månedlig forbrugsgrænse for din konto.",
|
||||
@@ -408,23 +408,23 @@ export const dict = {
|
||||
"workspace.monthlyLimit.noLimit": "Ingen forbrugsgrænse angivet.",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
|
||||
"workspace.reload.title": "Automatisk genindlæsning",
|
||||
"workspace.reload.disabled.before": "Automatisk genindlæsning er",
|
||||
"workspace.reload.disabled.state": "handicappet",
|
||||
"workspace.reload.disabled.after": "Aktiver for automatisk at genindlæse, når balancen er lav.",
|
||||
"workspace.reload.enabled.before": "Automatisk genindlæsning er",
|
||||
"workspace.reload.title": "Automatisk genopfyldning",
|
||||
"workspace.reload.disabled.before": "Automatisk genopfyldning er",
|
||||
"workspace.reload.disabled.state": "deaktiveret",
|
||||
"workspace.reload.disabled.after": "Aktiver for automatisk at genopfylde, når saldoen er lav.",
|
||||
"workspace.reload.enabled.before": "Automatisk genopfyldning er",
|
||||
"workspace.reload.enabled.state": "aktiveret",
|
||||
"workspace.reload.enabled.middle": "Vi genindlæser",
|
||||
"workspace.reload.enabled.middle": "Vi genopfylder",
|
||||
"workspace.reload.processingFee": "ekspeditionsgebyr",
|
||||
"workspace.reload.enabled.after": "når balancen er nået",
|
||||
"workspace.reload.edit": "Redigere",
|
||||
"workspace.reload.edit": "Rediger",
|
||||
"workspace.reload.enable": "Aktiver",
|
||||
"workspace.reload.enableAutoReload": "Aktiver automatisk genindlæsning",
|
||||
"workspace.reload.reloadAmount": "Genindlæs $",
|
||||
"workspace.reload.enableAutoReload": "Aktiver automatisk genopfyldning",
|
||||
"workspace.reload.reloadAmount": "Genopfyld $",
|
||||
"workspace.reload.whenBalanceReaches": "Når saldoen når $",
|
||||
"workspace.reload.saving": "Gemmer...",
|
||||
"workspace.reload.save": "Spare",
|
||||
"workspace.reload.failedAt": "Genindlæsning mislykkedes kl",
|
||||
"workspace.reload.save": "Gem",
|
||||
"workspace.reload.failedAt": "Genopfyldning mislykkedes kl",
|
||||
"workspace.reload.reason": "Årsag:",
|
||||
"workspace.reload.updatePaymentMethod": "Opdater din betalingsmetode, og prøv igen.",
|
||||
"workspace.reload.retrying": "Prøver igen...",
|
||||
@@ -434,10 +434,10 @@ export const dict = {
|
||||
"workspace.payments.table.date": "Dato",
|
||||
"workspace.payments.table.paymentId": "Betalings-id",
|
||||
"workspace.payments.table.amount": "Beløb",
|
||||
"workspace.payments.table.receipt": "Modtagelse",
|
||||
"workspace.payments.table.receipt": "Kvittering",
|
||||
"workspace.payments.type.credit": "kredit",
|
||||
"workspace.payments.type.subscription": "abonnement",
|
||||
"workspace.payments.view": "Udsigt",
|
||||
"workspace.payments.view": "Vis",
|
||||
"workspace.black.loading": "Indlæser...",
|
||||
"workspace.black.time.day": "dag",
|
||||
"workspace.black.time.days": "dage",
|
||||
@@ -458,8 +458,8 @@ export const dict = {
|
||||
"workspace.black.waitlist.ready": "Vi er klar til at tilmelde dig ${{plan}} per måned OpenCode Black plan.",
|
||||
"workspace.black.waitlist.leave": "Forlad venteliste",
|
||||
"workspace.black.waitlist.leaving": "Forlader...",
|
||||
"workspace.black.waitlist.left": "Venstre",
|
||||
"workspace.black.waitlist.enroll": "Indskrive",
|
||||
"workspace.black.waitlist.left": "Forladt",
|
||||
"workspace.black.waitlist.enroll": "Tilmeld",
|
||||
"workspace.black.waitlist.enrolling": "Tilmelder...",
|
||||
"workspace.black.waitlist.enrolled": "Tilmeldt",
|
||||
"workspace.black.waitlist.enrollNote":
|
||||
|
||||
@@ -306,27 +306,27 @@ export const dict = {
|
||||
"workspace.newUser.feature.lockin.title": "Kein Lock-in",
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"Verwenden Sie Zen mit einem beliebigen Codierungsagenten und nutzen Sie weiterhin andere Anbieter mit opencode, wann immer Sie möchten.",
|
||||
"workspace.newUser.copyApiKey": "Kopieren Sie den Schlüssel API",
|
||||
"workspace.newUser.copyApiKey": "API-Schlüssel kopieren",
|
||||
"workspace.newUser.copyKey": "Schlüssel kopieren",
|
||||
"workspace.newUser.copied": "Kopiert!",
|
||||
"workspace.newUser.step.enableBilling": "Abrechnung aktivieren",
|
||||
"workspace.newUser.step.login.before": "Laufen",
|
||||
"workspace.newUser.step.login.before": "Führe",
|
||||
"workspace.newUser.step.login.after": "und wählen Sie opencode",
|
||||
"workspace.newUser.step.pasteKey": "Fügen Sie Ihren API-Schlüssel ein",
|
||||
"workspace.newUser.step.models.before": "Starten Sie opencode und führen Sie es aus",
|
||||
"workspace.newUser.step.models.before": "Starte opencode und führe",
|
||||
"workspace.newUser.step.models.after": "um ein Modell auszuwählen",
|
||||
"workspace.models.title": "Modelle",
|
||||
"workspace.models.subtitle.beforeLink":
|
||||
"Verwalten Sie, auf welche Modelle Arbeitsbereichsmitglieder zugreifen können.",
|
||||
"workspace.models.table.model": "Modell",
|
||||
"workspace.models.table.enabled": "Ermöglicht",
|
||||
"workspace.models.table.enabled": "Aktiviert",
|
||||
"workspace.providers.title": "Bringen Sie Ihren eigenen Schlüssel mit",
|
||||
"workspace.providers.subtitle": "Konfigurieren Sie Ihre eigenen API-Schlüssel von KI-Anbietern.",
|
||||
"workspace.providers.placeholder": "Geben Sie den Schlüssel {{provider}} API ein ({{prefix}}...)",
|
||||
"workspace.providers.configure": "Konfigurieren",
|
||||
"workspace.providers.edit": "Bearbeiten",
|
||||
"workspace.providers.delete": "Löschen",
|
||||
"workspace.providers.saving": "Sparen...",
|
||||
"workspace.providers.saving": "Wird gespeichert...",
|
||||
"workspace.providers.save": "Speichern",
|
||||
"workspace.providers.table.provider": "Anbieter",
|
||||
"workspace.providers.table.apiKey": "API-Schlüssel",
|
||||
@@ -335,14 +335,14 @@ export const dict = {
|
||||
"workspace.usage.empty": "Machen Sie Ihren ersten API-Aufruf, um loszulegen.",
|
||||
"workspace.usage.table.date": "Datum",
|
||||
"workspace.usage.table.model": "Modell",
|
||||
"workspace.usage.table.input": "Eingang",
|
||||
"workspace.usage.table.output": "Ausgabe",
|
||||
"workspace.usage.table.input": "Input",
|
||||
"workspace.usage.table.output": "Output",
|
||||
"workspace.usage.table.cost": "Kosten",
|
||||
"workspace.usage.breakdown.input": "Eingang",
|
||||
"workspace.usage.breakdown.input": "Input",
|
||||
"workspace.usage.breakdown.cacheRead": "Cache-Lesen",
|
||||
"workspace.usage.breakdown.cacheWrite": "Cache-Schreiben",
|
||||
"workspace.usage.breakdown.output": "Ausgabe",
|
||||
"workspace.usage.breakdown.reasoning": "Argumentation",
|
||||
"workspace.usage.breakdown.output": "Output",
|
||||
"workspace.usage.breakdown.reasoning": "Reasoning",
|
||||
"workspace.usage.subscription": "Abonnement (${{amount}})",
|
||||
"workspace.cost.title": "Kosten",
|
||||
"workspace.cost.subtitle": "Nutzungskosten aufgeschlüsselt nach Modell.",
|
||||
@@ -360,12 +360,12 @@ export const dict = {
|
||||
"workspace.keys.table.key": "Schlüssel",
|
||||
"workspace.keys.table.createdBy": "Erstellt von",
|
||||
"workspace.keys.table.lastUsed": "Zuletzt verwendet",
|
||||
"workspace.keys.copyApiKey": "Kopieren Sie den Schlüssel API",
|
||||
"workspace.keys.copyApiKey": "API-Schlüssel kopieren",
|
||||
"workspace.keys.delete": "Löschen",
|
||||
"workspace.members.title": "Mitglieder",
|
||||
"workspace.members.subtitle": "Verwalten Sie Arbeitsbereichsmitglieder und ihre Berechtigungen.",
|
||||
"workspace.members.invite": "Mitglied einladen",
|
||||
"workspace.members.inviting": "Einladend...",
|
||||
"workspace.members.inviting": "Wird eingeladen...",
|
||||
"workspace.members.beta.beforeLink": "Während der Betaversion sind Arbeitsbereiche für Teams kostenlos.",
|
||||
"workspace.members.form.invitee": "Eingeladen",
|
||||
"workspace.members.form.emailPlaceholder": "Geben Sie Ihre E-Mail-Adresse ein",
|
||||
@@ -376,7 +376,7 @@ export const dict = {
|
||||
"workspace.members.invited": "eingeladen",
|
||||
"workspace.members.edit": "Bearbeiten",
|
||||
"workspace.members.delete": "Löschen",
|
||||
"workspace.members.saving": "Sparen...",
|
||||
"workspace.members.saving": "Wird gespeichert...",
|
||||
"workspace.members.save": "Speichern",
|
||||
"workspace.members.table.email": "E-Mail",
|
||||
"workspace.members.table.role": "Rolle",
|
||||
@@ -408,30 +408,30 @@ export const dict = {
|
||||
"workspace.monthlyLimit.title": "Monatliches Limit",
|
||||
"workspace.monthlyLimit.subtitle": "Legen Sie ein monatliches Nutzungslimit für Ihr Konto fest.",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "Einstellung...",
|
||||
"workspace.monthlyLimit.set": "Satz",
|
||||
"workspace.monthlyLimit.setting": "Wird gesetzt...",
|
||||
"workspace.monthlyLimit.set": "Festlegen",
|
||||
"workspace.monthlyLimit.edit": "Limit bearbeiten",
|
||||
"workspace.monthlyLimit.noLimit": "Kein Nutzungslimit festgelegt.",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "ist $",
|
||||
"workspace.reload.title": "Automatisches Neuladen",
|
||||
"workspace.reload.disabled.before": "Automatisches Nachladen ist",
|
||||
"workspace.reload.title": "Automatische Aufladung",
|
||||
"workspace.reload.disabled.before": "Automatische Aufladung ist",
|
||||
"workspace.reload.disabled.state": "deaktiviert",
|
||||
"workspace.reload.disabled.after":
|
||||
"Aktivieren Sie diese Option, um das Guthaben automatisch neu zu laden, wenn das Guthaben niedrig ist.",
|
||||
"workspace.reload.enabled.before": "Automatisches Nachladen ist",
|
||||
"workspace.reload.enabled.state": "ermöglicht",
|
||||
"workspace.reload.enabled.middle": "Wir laden nach",
|
||||
"Aktivieren Sie diese Option, damit bei niedrigem Kontostand automatisch aufgeladen wird.",
|
||||
"workspace.reload.enabled.before": "Automatische Aufladung ist",
|
||||
"workspace.reload.enabled.state": "aktiviert",
|
||||
"workspace.reload.enabled.middle": "Wir laden auf",
|
||||
"workspace.reload.processingFee": "Bearbeitungsgebühr",
|
||||
"workspace.reload.enabled.after": "wenn das Gleichgewicht erreicht ist",
|
||||
"workspace.reload.enabled.after": "sobald der Kontostand",
|
||||
"workspace.reload.edit": "Bearbeiten",
|
||||
"workspace.reload.enable": "Aktivieren",
|
||||
"workspace.reload.enableAutoReload": "Aktivieren Sie das automatische Neuladen",
|
||||
"workspace.reload.reloadAmount": "$ neu laden",
|
||||
"workspace.reload.whenBalanceReaches": "Wenn der Saldo $ erreicht",
|
||||
"workspace.reload.saving": "Sparen...",
|
||||
"workspace.reload.enableAutoReload": "Automatische Aufladung aktivieren",
|
||||
"workspace.reload.reloadAmount": "Aufladebetrag $",
|
||||
"workspace.reload.whenBalanceReaches": "Wenn der Kontostand $ erreicht",
|
||||
"workspace.reload.saving": "Wird gespeichert...",
|
||||
"workspace.reload.save": "Speichern",
|
||||
"workspace.reload.failedAt": "Neuladen fehlgeschlagen bei",
|
||||
"workspace.reload.failedAt": "Aufladung fehlgeschlagen am",
|
||||
"workspace.reload.reason": "Grund:",
|
||||
"workspace.reload.updatePaymentMethod": "Bitte aktualisieren Sie Ihre Zahlungsmethode und versuchen Sie es erneut.",
|
||||
"workspace.reload.retrying": "Erneuter Versuch...",
|
||||
@@ -440,11 +440,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "Letzte Zahlungsvorgänge.",
|
||||
"workspace.payments.table.date": "Datum",
|
||||
"workspace.payments.table.paymentId": "Zahlungs-ID",
|
||||
"workspace.payments.table.amount": "Menge",
|
||||
"workspace.payments.table.amount": "Betrag",
|
||||
"workspace.payments.table.receipt": "Quittung",
|
||||
"workspace.payments.type.credit": "Kredit",
|
||||
"workspace.payments.type.subscription": "Abonnement",
|
||||
"workspace.payments.view": "Sicht",
|
||||
"workspace.payments.view": "Anzeigen",
|
||||
"workspace.black.loading": "Laden...",
|
||||
"workspace.black.time.day": "Tag",
|
||||
"workspace.black.time.days": "Tage",
|
||||
@@ -454,21 +454,21 @@ export const dict = {
|
||||
"workspace.black.time.minutes": "Minuten",
|
||||
"workspace.black.time.fewSeconds": "ein paar Sekunden",
|
||||
"workspace.black.subscription.title": "Abonnement",
|
||||
"workspace.black.subscription.message": "Sie haben OpenCode Black für {{plan}} pro Monat abonniert.",
|
||||
"workspace.black.subscription.message": "Sie haben OpenCode Black für ${{plan}} pro Monat abonniert.",
|
||||
"workspace.black.subscription.manage": "Abonnement verwalten",
|
||||
"workspace.black.subscription.rollingUsage": "5-stündige Nutzung",
|
||||
"workspace.black.subscription.weeklyUsage": "Wöchentliche Nutzung",
|
||||
"workspace.black.subscription.resetsIn": "Wird zurückgesetzt",
|
||||
"workspace.black.subscription.resetsIn": "Zurückgesetzt in",
|
||||
"workspace.black.subscription.useBalance":
|
||||
"Nutzen Sie Ihr verfügbares Guthaben, nachdem Sie die Nutzungslimits erreicht haben",
|
||||
"workspace.black.waitlist.title": "Warteliste",
|
||||
"workspace.black.waitlist.joined":
|
||||
"Sie stehen auf der Warteliste für den Black-Plan im Wert von ${{plan}} pro Monat OpenCode.",
|
||||
"Sie stehen auf der Warteliste für den OpenCode Black Tarif für ${{plan}} pro Monat.",
|
||||
"workspace.black.waitlist.ready":
|
||||
"Wir sind bereit, Sie für den Black-Plan im Wert von ${{plan}} pro Monat OpenCode anzumelden.",
|
||||
"Wir können Sie jetzt in den OpenCode Black Tarif für ${{plan}} pro Monat aufnehmen.",
|
||||
"workspace.black.waitlist.leave": "Warteliste verlassen",
|
||||
"workspace.black.waitlist.leaving": "Verlassen...",
|
||||
"workspace.black.waitlist.left": "Links",
|
||||
"workspace.black.waitlist.left": "Verlassen",
|
||||
"workspace.black.waitlist.enroll": "Einschreiben",
|
||||
"workspace.black.waitlist.enrolling": "Anmeldung...",
|
||||
"workspace.black.waitlist.enrolled": "Eingeschrieben",
|
||||
|
||||
@@ -394,7 +394,7 @@ export const dict = {
|
||||
"workspace.settings.edit": "Edit",
|
||||
|
||||
"workspace.billing.title": "Billing",
|
||||
"workspace.billing.subtitle.beforeLink": "Manage payments methods.",
|
||||
"workspace.billing.subtitle.beforeLink": "Manage payment methods.",
|
||||
"workspace.billing.contactUs": "Contact us",
|
||||
"workspace.billing.subtitle.afterLink": "if you have any questions.",
|
||||
"workspace.billing.currentBalance": "Current Balance",
|
||||
|
||||
@@ -284,8 +284,8 @@ export const dict = {
|
||||
"changelog.hero.subtitle": "Nuovi aggiornamenti e miglioramenti per OpenCode",
|
||||
"changelog.empty": "Nessuna voce di changelog trovata.",
|
||||
"changelog.viewJson": "Visualizza JSON",
|
||||
"workspace.nav.zen": "zen",
|
||||
"workspace.nav.apiKeys": "API Chiavi",
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.apiKeys": "Chiavi API",
|
||||
"workspace.nav.members": "Membri",
|
||||
"workspace.nav.billing": "Fatturazione",
|
||||
"workspace.nav.settings": "Impostazioni",
|
||||
@@ -299,14 +299,14 @@ export const dict = {
|
||||
"workspace.newUser.feature.quality.title": "Massima qualità",
|
||||
"workspace.newUser.feature.quality.body":
|
||||
"Modelli di accesso configurati per prestazioni ottimali: senza downgrade o instradamento verso fornitori più economici.",
|
||||
"workspace.newUser.feature.lockin.title": "Nessun blocco",
|
||||
"workspace.newUser.feature.lockin.title": "Nessun lock-in",
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"Utilizza Zen con qualsiasi agente di codifica e continua a utilizzare altri provider con opencode ogni volta che vuoi.",
|
||||
"workspace.newUser.copyApiKey": "Copia la chiave API",
|
||||
"workspace.newUser.copyKey": "Copia chiave",
|
||||
"workspace.newUser.copied": "Copiato!",
|
||||
"workspace.newUser.step.enableBilling": "Abilita fatturazione",
|
||||
"workspace.newUser.step.login.before": "Correre",
|
||||
"workspace.newUser.step.login.before": "Esegui",
|
||||
"workspace.newUser.step.login.after": "e seleziona opencode",
|
||||
"workspace.newUser.step.pasteKey": "Incolla la tua chiave API",
|
||||
"workspace.newUser.step.models.before": "Avvia opencode ed esegui",
|
||||
@@ -315,16 +315,16 @@ export const dict = {
|
||||
"workspace.models.subtitle.beforeLink": "Gestire i modelli a cui possono accedere i membri dell'area di lavoro.",
|
||||
"workspace.models.table.model": "Modello",
|
||||
"workspace.models.table.enabled": "Abilitato",
|
||||
"workspace.providers.title": "Porta la tua chiave",
|
||||
"workspace.providers.title": "Bring Your Own Key (BYOK)",
|
||||
"workspace.providers.subtitle": "Configura le tue chiavi API dai fornitori di intelligenza artificiale.",
|
||||
"workspace.providers.placeholder": "Inserisci la chiave {{provider}} API ({{prefix}}...)",
|
||||
"workspace.providers.configure": "Configura",
|
||||
"workspace.providers.edit": "Modificare",
|
||||
"workspace.providers.delete": "Eliminare",
|
||||
"workspace.providers.saving": "Risparmio...",
|
||||
"workspace.providers.saving": "Salvataggio in corso...",
|
||||
"workspace.providers.save": "Salva",
|
||||
"workspace.providers.table.provider": "Fornitore",
|
||||
"workspace.providers.table.apiKey": "API Chiave",
|
||||
"workspace.providers.table.apiKey": "Chiave API",
|
||||
"workspace.usage.title": "Cronologia dell'utilizzo",
|
||||
"workspace.usage.subtitle": "Utilizzo e costi recenti di API.",
|
||||
"workspace.usage.empty": "Effettua la tua prima chiamata API per iniziare.",
|
||||
@@ -346,7 +346,7 @@ export const dict = {
|
||||
"workspace.cost.deletedSuffix": "(eliminato)",
|
||||
"workspace.cost.empty": "Nessun dato di utilizzo disponibile per il periodo selezionato.",
|
||||
"workspace.cost.subscriptionShort": "sub",
|
||||
"workspace.keys.title": "API Chiavi",
|
||||
"workspace.keys.title": "Chiavi API",
|
||||
"workspace.keys.subtitle": "Gestisci le tue chiavi API per accedere ai servizi opencode.",
|
||||
"workspace.keys.create": "Crea chiave API",
|
||||
"workspace.keys.placeholder": "Inserisci il nome della chiave",
|
||||
@@ -360,7 +360,7 @@ export const dict = {
|
||||
"workspace.members.title": "Membri",
|
||||
"workspace.members.subtitle": "Gestire i membri dell'area di lavoro e le relative autorizzazioni.",
|
||||
"workspace.members.invite": "Invita membro",
|
||||
"workspace.members.inviting": "Invitante...",
|
||||
"workspace.members.inviting": "Invito in corso...",
|
||||
"workspace.members.beta.beforeLink": "Gli spazi di lavoro sono gratuiti per i team durante la beta.",
|
||||
"workspace.members.form.invitee": "Invitato",
|
||||
"workspace.members.form.emailPlaceholder": "Inserisci l'e-mail",
|
||||
@@ -371,12 +371,12 @@ export const dict = {
|
||||
"workspace.members.invited": "invitato",
|
||||
"workspace.members.edit": "Modificare",
|
||||
"workspace.members.delete": "Eliminare",
|
||||
"workspace.members.saving": "Risparmio...",
|
||||
"workspace.members.saving": "Salvataggio in corso...",
|
||||
"workspace.members.save": "Salva",
|
||||
"workspace.members.table.email": "E-mail",
|
||||
"workspace.members.table.role": "Ruolo",
|
||||
"workspace.members.table.monthLimit": "Limite mensile",
|
||||
"workspace.members.role.admin": "Ammin",
|
||||
"workspace.members.role.admin": "Admin",
|
||||
"workspace.members.role.adminDescription": "Può gestire modelli, membri e fatturazione",
|
||||
"workspace.members.role.member": "Membro",
|
||||
"workspace.members.role.memberDescription": "Possono generare chiavi API solo per se stessi",
|
||||
@@ -388,42 +388,42 @@ export const dict = {
|
||||
"workspace.settings.save": "Salva",
|
||||
"workspace.settings.edit": "Modificare",
|
||||
"workspace.billing.title": "Fatturazione",
|
||||
"workspace.billing.subtitle.beforeLink": "Gestire i metodi di pagamento.",
|
||||
"workspace.billing.subtitle.beforeLink": "Gestisci i metodi di pagamento.",
|
||||
"workspace.billing.contactUs": "Contattaci",
|
||||
"workspace.billing.subtitle.afterLink": "se hai qualche domanda",
|
||||
"workspace.billing.currentBalance": "Saldo attuale",
|
||||
"workspace.billing.add": "Aggiungi $",
|
||||
"workspace.billing.enterAmount": "Inserisci l'importo",
|
||||
"workspace.billing.loading": "Caricamento...",
|
||||
"workspace.billing.addAction": "Aggiungere",
|
||||
"workspace.billing.addAction": "Aggiungi",
|
||||
"workspace.billing.addBalance": "Aggiungi saldo",
|
||||
"workspace.billing.linkedToStripe": "Collegato a Stripe",
|
||||
"workspace.billing.manage": "Maneggio",
|
||||
"workspace.billing.manage": "Gestisci",
|
||||
"workspace.billing.enable": "Abilita fatturazione",
|
||||
"workspace.monthlyLimit.title": "Limite mensile",
|
||||
"workspace.monthlyLimit.subtitle": "Imposta un limite di utilizzo mensile per il tuo account.",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "Collocamento...",
|
||||
"workspace.monthlyLimit.setting": "Impostazione in corso...",
|
||||
"workspace.monthlyLimit.set": "Impostato",
|
||||
"workspace.monthlyLimit.edit": "Modifica limite",
|
||||
"workspace.monthlyLimit.noLimit": "Nessun limite di utilizzo impostato.",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "è $",
|
||||
"workspace.reload.title": "Ricarica automatica",
|
||||
"workspace.reload.disabled.before": "La ricarica automatica lo è",
|
||||
"workspace.reload.disabled.before": "La ricarica automatica è",
|
||||
"workspace.reload.disabled.state": "disabilitato",
|
||||
"workspace.reload.disabled.after": "Abilita la ricarica automatica quando il saldo è basso.",
|
||||
"workspace.reload.enabled.before": "La ricarica automatica lo è",
|
||||
"workspace.reload.enabled.before": "La ricarica automatica è",
|
||||
"workspace.reload.enabled.state": "abilitato",
|
||||
"workspace.reload.enabled.middle": "Ricaricheremo",
|
||||
"workspace.reload.processingFee": "tassa di elaborazione",
|
||||
"workspace.reload.enabled.after": "quando l'equilibrio raggiunge",
|
||||
"workspace.reload.enabled.after": "quando il saldo raggiunge",
|
||||
"workspace.reload.edit": "Modificare",
|
||||
"workspace.reload.enable": "Abilitare",
|
||||
"workspace.reload.enableAutoReload": "Abilita ricarica automatica",
|
||||
"workspace.reload.reloadAmount": "Ricarica $",
|
||||
"workspace.reload.whenBalanceReaches": "Quando il saldo raggiunge $",
|
||||
"workspace.reload.saving": "Risparmio...",
|
||||
"workspace.reload.saving": "Salvataggio in corso...",
|
||||
"workspace.reload.save": "Salva",
|
||||
"workspace.reload.failedAt": "Ricarica non riuscita a",
|
||||
"workspace.reload.reason": "Motivo:",
|
||||
@@ -434,11 +434,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "Transazioni di pagamento recenti.",
|
||||
"workspace.payments.table.date": "Data",
|
||||
"workspace.payments.table.paymentId": "ID pagamento",
|
||||
"workspace.payments.table.amount": "Quantità",
|
||||
"workspace.payments.table.amount": "Importo",
|
||||
"workspace.payments.table.receipt": "Ricevuta",
|
||||
"workspace.payments.type.credit": "credito",
|
||||
"workspace.payments.type.subscription": "sottoscrizione",
|
||||
"workspace.payments.view": "Visualizzazione",
|
||||
"workspace.payments.view": "Visualizza",
|
||||
"workspace.black.loading": "Caricamento...",
|
||||
"workspace.black.time.day": "giorno",
|
||||
"workspace.black.time.days": "giorni",
|
||||
@@ -452,14 +452,14 @@ export const dict = {
|
||||
"workspace.black.subscription.manage": "Gestisci abbonamento",
|
||||
"workspace.black.subscription.rollingUsage": "Utilizzo di 5 ore",
|
||||
"workspace.black.subscription.weeklyUsage": "Utilizzo settimanale",
|
||||
"workspace.black.subscription.resetsIn": "Si reimposta",
|
||||
"workspace.black.subscription.resetsIn": "Si reimposta tra",
|
||||
"workspace.black.subscription.useBalance": "Utilizza il saldo disponibile dopo aver raggiunto i limiti di utilizzo",
|
||||
"workspace.black.waitlist.title": "Lista d'attesa",
|
||||
"workspace.black.waitlist.joined": "Sei in lista d'attesa per il piano nero ${{plan}} al mese OpenCode.",
|
||||
"workspace.black.waitlist.joined": "Sei in lista d'attesa per il piano OpenCode Black da ${{plan}} al mese.",
|
||||
"workspace.black.waitlist.ready": "Siamo pronti per iscriverti al piano OpenCode Black da ${{plan}} al mese.",
|
||||
"workspace.black.waitlist.leave": "Lascia la lista d'attesa",
|
||||
"workspace.black.waitlist.leaving": "In partenza...",
|
||||
"workspace.black.waitlist.left": "Sinistra",
|
||||
"workspace.black.waitlist.leaving": "Uscita in corso...",
|
||||
"workspace.black.waitlist.left": "Uscito dalla lista d'attesa",
|
||||
"workspace.black.waitlist.enroll": "Iscriversi",
|
||||
"workspace.black.waitlist.enrolling": "Iscrizione...",
|
||||
"workspace.black.waitlist.enrolled": "Iscritto",
|
||||
|
||||
@@ -203,7 +203,7 @@ export const dict = {
|
||||
"zen.how.step2.link": "betale per forespørsel",
|
||||
"zen.how.step2.afterLink": "med null markeringer",
|
||||
"zen.how.step3.title": "Automatisk påfylling",
|
||||
"zen.how.step3.body": "når saldoen din når $5, legger vi automatisk til $20",
|
||||
"zen.how.step3.body": "når saldoen din når $5, fyller vi automatisk på $20",
|
||||
"zen.privacy.title": "Personvernet ditt er viktig for oss",
|
||||
"zen.privacy.beforeExceptions":
|
||||
"Alle Zen-modeller er vert i USA. Leverandører følger en nulloppbevaringspolicy og bruker ikke dataene dine til modelltrening, med",
|
||||
@@ -283,7 +283,7 @@ export const dict = {
|
||||
"changelog.empty": "Ingen endringsloggoppforinger funnet.",
|
||||
"changelog.viewJson": "Vis JSON",
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.apiKeys": "API Taster",
|
||||
"workspace.nav.apiKeys": "API Nøkler",
|
||||
"workspace.nav.members": "Medlemmer",
|
||||
"workspace.nav.billing": "Fakturering",
|
||||
"workspace.nav.settings": "Innstillinger",
|
||||
@@ -320,7 +320,7 @@ export const dict = {
|
||||
"workspace.providers.edit": "Redigere",
|
||||
"workspace.providers.delete": "Slett",
|
||||
"workspace.providers.saving": "Lagrer...",
|
||||
"workspace.providers.save": "Spare",
|
||||
"workspace.providers.save": "Lagre",
|
||||
"workspace.providers.table.provider": "Leverandør",
|
||||
"workspace.providers.table.apiKey": "API nøkkel",
|
||||
"workspace.usage.title": "Brukshistorikk",
|
||||
@@ -330,21 +330,21 @@ export const dict = {
|
||||
"workspace.usage.table.model": "Modell",
|
||||
"workspace.usage.table.input": "Inndata",
|
||||
"workspace.usage.table.output": "Produksjon",
|
||||
"workspace.usage.table.cost": "Koste",
|
||||
"workspace.usage.table.cost": "Kostnad",
|
||||
"workspace.usage.breakdown.input": "Inndata",
|
||||
"workspace.usage.breakdown.cacheRead": "Cache lest",
|
||||
"workspace.usage.breakdown.cacheWrite": "Cache-skriving",
|
||||
"workspace.usage.breakdown.output": "Produksjon",
|
||||
"workspace.usage.breakdown.reasoning": "Argumentasjon",
|
||||
"workspace.usage.subscription": "abonnement (${{amount}})",
|
||||
"workspace.cost.title": "Koste",
|
||||
"workspace.cost.title": "Kostnad",
|
||||
"workspace.cost.subtitle": "Brukskostnader fordelt på modell.",
|
||||
"workspace.cost.allModels": "Alle modeller",
|
||||
"workspace.cost.allKeys": "Alle nøkler",
|
||||
"workspace.cost.deletedSuffix": "(slettet)",
|
||||
"workspace.cost.empty": "Ingen bruksdata tilgjengelig for den valgte perioden.",
|
||||
"workspace.cost.subscriptionShort": "sub",
|
||||
"workspace.keys.title": "API Taster",
|
||||
"workspace.keys.title": "API Nøkler",
|
||||
"workspace.keys.subtitle": "Administrer API-nøklene dine for å få tilgang til opencode-tjenester.",
|
||||
"workspace.keys.create": "Opprett API-nøkkel",
|
||||
"workspace.keys.placeholder": "Skriv inn nøkkelnavn",
|
||||
@@ -370,7 +370,7 @@ export const dict = {
|
||||
"workspace.members.edit": "Redigere",
|
||||
"workspace.members.delete": "Slett",
|
||||
"workspace.members.saving": "Lagrer...",
|
||||
"workspace.members.save": "Spare",
|
||||
"workspace.members.save": "Lagre",
|
||||
"workspace.members.table.email": "E-post",
|
||||
"workspace.members.table.role": "Rolle",
|
||||
"workspace.members.table.monthLimit": "Månedsgrense",
|
||||
@@ -383,7 +383,7 @@ export const dict = {
|
||||
"workspace.settings.workspaceName": "Navn på arbeidsområde",
|
||||
"workspace.settings.defaultName": "Misligholde",
|
||||
"workspace.settings.updating": "Oppdaterer...",
|
||||
"workspace.settings.save": "Spare",
|
||||
"workspace.settings.save": "Lagre",
|
||||
"workspace.settings.edit": "Redigere",
|
||||
"workspace.billing.title": "Fakturering",
|
||||
"workspace.billing.subtitle.beforeLink": "Administrer betalingsmåter.",
|
||||
@@ -407,22 +407,22 @@ export const dict = {
|
||||
"workspace.monthlyLimit.noLimit": "Ingen bruksgrense satt.",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Gjeldende bruk for",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
|
||||
"workspace.reload.title": "Last inn automatisk",
|
||||
"workspace.reload.disabled.before": "Automatisk reload er",
|
||||
"workspace.reload.disabled.state": "funksjonshemmet",
|
||||
"workspace.reload.disabled.after": "Aktiver for å laste automatisk på nytt når balansen er lav.",
|
||||
"workspace.reload.enabled.before": "Automatisk reload er",
|
||||
"workspace.reload.title": "Automatisk påfylling",
|
||||
"workspace.reload.disabled.before": "Automatisk påfylling er",
|
||||
"workspace.reload.disabled.state": "deaktivert",
|
||||
"workspace.reload.disabled.after": "Aktiver for å automatisk påfylle på nytt når saldoen er lav.",
|
||||
"workspace.reload.enabled.before": "Automatisk påfylling er",
|
||||
"workspace.reload.enabled.state": "aktivert",
|
||||
"workspace.reload.enabled.middle": "Vi laster på nytt",
|
||||
"workspace.reload.enabled.middle": "Vi fyller på",
|
||||
"workspace.reload.processingFee": "behandlingsgebyr",
|
||||
"workspace.reload.enabled.after": "når balansen når",
|
||||
"workspace.reload.enabled.after": "når saldoen når",
|
||||
"workspace.reload.edit": "Redigere",
|
||||
"workspace.reload.enable": "Aktiver",
|
||||
"workspace.reload.enableAutoReload": "Aktiver automatisk reload",
|
||||
"workspace.reload.enableAutoReload": "Aktiver automatisk påfylling",
|
||||
"workspace.reload.reloadAmount": "Last inn $",
|
||||
"workspace.reload.whenBalanceReaches": "Når saldoen når $",
|
||||
"workspace.reload.saving": "Lagrer...",
|
||||
"workspace.reload.save": "Spare",
|
||||
"workspace.reload.save": "Lagre",
|
||||
"workspace.reload.failedAt": "Omlasting mislyktes kl",
|
||||
"workspace.reload.reason": "Grunn:",
|
||||
"workspace.reload.updatePaymentMethod": "Oppdater betalingsmåten og prøv på nytt.",
|
||||
@@ -436,7 +436,7 @@ export const dict = {
|
||||
"workspace.payments.table.receipt": "Kvittering",
|
||||
"workspace.payments.type.credit": "kreditt",
|
||||
"workspace.payments.type.subscription": "abonnement",
|
||||
"workspace.payments.view": "Utsikt",
|
||||
"workspace.payments.view": "Vis",
|
||||
"workspace.black.loading": "Laster inn...",
|
||||
"workspace.black.time.day": "dag",
|
||||
"workspace.black.time.days": "dager",
|
||||
|
||||
@@ -293,9 +293,9 @@ export const dict = {
|
||||
"changelog.hero.subtitle": "OpenCode \u7684\u65b0\u66f4\u65b0\u4e0e\u6539\u8fdb",
|
||||
"changelog.empty": "\u672a\u627e\u5230\u66f4\u65b0\u65e5\u5fd7\u6761\u76ee\u3002",
|
||||
"changelog.viewJson": "\u67e5\u770b JSON",
|
||||
"workspace.nav.zen": "禅",
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.apiKeys": "API 键",
|
||||
"workspace.nav.members": "会员",
|
||||
"workspace.nav.members": "成员",
|
||||
"workspace.nav.billing": "计费",
|
||||
"workspace.nav.settings": "设置",
|
||||
"workspace.home.banner.beforeLink": "编码代理的可靠优化模型。",
|
||||
@@ -310,26 +310,26 @@ export const dict = {
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"将 Zen 与任何编码代理结合使用,并在需要时继续将其他提供程序与 opencode 结合使用。",
|
||||
"workspace.newUser.copyApiKey": "复制 API 密钥",
|
||||
"workspace.newUser.copyKey": "复制钥匙",
|
||||
"workspace.newUser.copied": "复制了!",
|
||||
"workspace.newUser.copyKey": "复制密钥",
|
||||
"workspace.newUser.copied": "已复制!",
|
||||
"workspace.newUser.step.enableBilling": "启用计费",
|
||||
"workspace.newUser.step.login.before": "跑步",
|
||||
"workspace.newUser.step.login.before": "运行",
|
||||
"workspace.newUser.step.login.after": "并选择 opencode",
|
||||
"workspace.newUser.step.pasteKey": "粘贴您的 API 密钥",
|
||||
"workspace.newUser.step.models.before": "启动 opencode 并运行",
|
||||
"workspace.newUser.step.models.after": "选择型号",
|
||||
"workspace.models.title": "型号",
|
||||
"workspace.newUser.step.models.after": "选择模型",
|
||||
"workspace.models.title": "模型",
|
||||
"workspace.models.subtitle.beforeLink": "管理工作区成员可以访问哪些模型。",
|
||||
"workspace.models.table.model": "模型",
|
||||
"workspace.models.table.enabled": "启用",
|
||||
"workspace.providers.title": "带上你自己的钥匙",
|
||||
"workspace.providers.title": "自带密钥",
|
||||
"workspace.providers.subtitle": "从 AI 提供商处配置您自己的 API 密钥。",
|
||||
"workspace.providers.placeholder": "输入 {{provider}} API 密钥({{prefix}}...)",
|
||||
"workspace.providers.configure": "配置",
|
||||
"workspace.providers.edit": "编辑",
|
||||
"workspace.providers.delete": "删除",
|
||||
"workspace.providers.saving": "保存...",
|
||||
"workspace.providers.save": "节省",
|
||||
"workspace.providers.save": "保存",
|
||||
"workspace.providers.table.provider": "提供者",
|
||||
"workspace.providers.table.apiKey": "API 密钥",
|
||||
"workspace.usage.title": "使用历史",
|
||||
@@ -348,25 +348,25 @@ export const dict = {
|
||||
"workspace.usage.subscription": "订阅 (${{amount}})",
|
||||
"workspace.cost.title": "成本",
|
||||
"workspace.cost.subtitle": "按型号细分的使用成本。",
|
||||
"workspace.cost.allModels": "所有型号",
|
||||
"workspace.cost.allKeys": "所有按键",
|
||||
"workspace.cost.allModels": "所有模型",
|
||||
"workspace.cost.allKeys": "所有密钥",
|
||||
"workspace.cost.deletedSuffix": "(已删除)",
|
||||
"workspace.cost.empty": "所选期间没有可用的使用数据。",
|
||||
"workspace.cost.subscriptionShort": "子",
|
||||
"workspace.cost.subscriptionShort": "订",
|
||||
"workspace.keys.title": "API 键",
|
||||
"workspace.keys.subtitle": "管理您的 API 密钥以访问 opencode 服务。",
|
||||
"workspace.keys.create": "创建 API 密钥",
|
||||
"workspace.keys.placeholder": "输入按键名称",
|
||||
"workspace.keys.placeholder": "输入密钥名称",
|
||||
"workspace.keys.empty": "创建 opencode 网关 API 密钥",
|
||||
"workspace.keys.table.name": "姓名",
|
||||
"workspace.keys.table.key": "钥匙",
|
||||
"workspace.keys.table.name": "名称",
|
||||
"workspace.keys.table.key": "密钥",
|
||||
"workspace.keys.table.createdBy": "创建者",
|
||||
"workspace.keys.table.lastUsed": "最后使用",
|
||||
"workspace.keys.copyApiKey": "复制 API 密钥",
|
||||
"workspace.keys.delete": "删除",
|
||||
"workspace.members.title": "会员",
|
||||
"workspace.members.title": "成员",
|
||||
"workspace.members.subtitle": "管理工作区成员及其权限。",
|
||||
"workspace.members.invite": "邀请会员",
|
||||
"workspace.members.invite": "邀请成员",
|
||||
"workspace.members.inviting": "邀请...",
|
||||
"workspace.members.beta.beforeLink": "测试期间,工作空间对团队免费。",
|
||||
"workspace.members.form.invitee": "受邀者",
|
||||
@@ -379,11 +379,11 @@ export const dict = {
|
||||
"workspace.members.edit": "编辑",
|
||||
"workspace.members.delete": "删除",
|
||||
"workspace.members.saving": "保存...",
|
||||
"workspace.members.save": "节省",
|
||||
"workspace.members.save": "保存",
|
||||
"workspace.members.table.email": "电子邮件",
|
||||
"workspace.members.table.role": "角色",
|
||||
"workspace.members.table.monthLimit": "月份限制",
|
||||
"workspace.members.role.admin": "行政",
|
||||
"workspace.members.table.monthLimit": "月限额",
|
||||
"workspace.members.role.admin": "管理员",
|
||||
"workspace.members.role.adminDescription": "可以管理模型、成员和计费",
|
||||
"workspace.members.role.member": "成员",
|
||||
"workspace.members.role.memberDescription": "只能为自己生成 API 密钥",
|
||||
@@ -392,7 +392,7 @@ export const dict = {
|
||||
"workspace.settings.workspaceName": "工作区名称",
|
||||
"workspace.settings.defaultName": "默认",
|
||||
"workspace.settings.updating": "更新中...",
|
||||
"workspace.settings.save": "节省",
|
||||
"workspace.settings.save": "保存",
|
||||
"workspace.settings.edit": "编辑",
|
||||
"workspace.billing.title": "计费",
|
||||
"workspace.billing.subtitle.beforeLink": "管理付款方式。",
|
||||
@@ -404,35 +404,35 @@ export const dict = {
|
||||
"workspace.billing.loading": "加载中...",
|
||||
"workspace.billing.addAction": "添加",
|
||||
"workspace.billing.addBalance": "添加余额",
|
||||
"workspace.billing.linkedToStripe": "链接到条纹",
|
||||
"workspace.billing.linkedToStripe": "已绑定 Stripe",
|
||||
"workspace.billing.manage": "管理",
|
||||
"workspace.billing.enable": "启用计费",
|
||||
"workspace.monthlyLimit.title": "每月限额",
|
||||
"workspace.monthlyLimit.subtitle": "为您的帐户设置每月使用限额。",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "环境...",
|
||||
"workspace.monthlyLimit.set": "放",
|
||||
"workspace.monthlyLimit.setting": "设置中...",
|
||||
"workspace.monthlyLimit.set": "设置",
|
||||
"workspace.monthlyLimit.edit": "编辑限制",
|
||||
"workspace.monthlyLimit.noLimit": "没有设置使用限制。",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前使用情况为",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "是 $",
|
||||
"workspace.reload.title": "自动重新加载",
|
||||
"workspace.reload.disabled.before": "自动重新加载是",
|
||||
"workspace.reload.disabled.state": "残疾人",
|
||||
"workspace.reload.disabled.after": "启用余额不足时自动充值。",
|
||||
"workspace.reload.enabled.before": "自动重新加载是",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $",
|
||||
"workspace.reload.title": "自动充值",
|
||||
"workspace.reload.disabled.before": "自动充值已",
|
||||
"workspace.reload.disabled.state": "停用",
|
||||
"workspace.reload.disabled.after": "启用后将在余额较低时自动充值。",
|
||||
"workspace.reload.enabled.before": "自动充值已",
|
||||
"workspace.reload.enabled.state": "已启用",
|
||||
"workspace.reload.enabled.middle": "我们将重新加载",
|
||||
"workspace.reload.processingFee": "加工费",
|
||||
"workspace.reload.enabled.middle": "我们将自动充值",
|
||||
"workspace.reload.processingFee": "手续费",
|
||||
"workspace.reload.enabled.after": "当余额达到",
|
||||
"workspace.reload.edit": "编辑",
|
||||
"workspace.reload.enable": "使能够",
|
||||
"workspace.reload.enableAutoReload": "启用自动重新加载",
|
||||
"workspace.reload.reloadAmount": "重新加载 $",
|
||||
"workspace.reload.enable": "启用",
|
||||
"workspace.reload.enableAutoReload": "启用自动充值",
|
||||
"workspace.reload.reloadAmount": "充值 $",
|
||||
"workspace.reload.whenBalanceReaches": "当余额达到 $",
|
||||
"workspace.reload.saving": "保存...",
|
||||
"workspace.reload.save": "节省",
|
||||
"workspace.reload.failedAt": "重新加载失败于",
|
||||
"workspace.reload.save": "保存",
|
||||
"workspace.reload.failedAt": "充值失败于",
|
||||
"workspace.reload.reason": "原因:",
|
||||
"workspace.reload.updatePaymentMethod": "请更新您的付款方式并重试。",
|
||||
"workspace.reload.retrying": "正在重试...",
|
||||
@@ -441,11 +441,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "最近的付款交易。",
|
||||
"workspace.payments.table.date": "日期",
|
||||
"workspace.payments.table.paymentId": "付款ID",
|
||||
"workspace.payments.table.amount": "数量",
|
||||
"workspace.payments.table.amount": "金额",
|
||||
"workspace.payments.table.receipt": "收据",
|
||||
"workspace.payments.type.credit": "信用",
|
||||
"workspace.payments.type.subscription": "订阅",
|
||||
"workspace.payments.view": "看法",
|
||||
"workspace.payments.view": "查看",
|
||||
"workspace.black.loading": "加载中...",
|
||||
"workspace.black.time.day": "天",
|
||||
"workspace.black.time.days": "天",
|
||||
@@ -455,20 +455,20 @@ export const dict = {
|
||||
"workspace.black.time.minutes": "分钟",
|
||||
"workspace.black.time.fewSeconds": "几秒钟",
|
||||
"workspace.black.subscription.title": "订阅",
|
||||
"workspace.black.subscription.message": "您已订阅 OpenCode Black,每月费用为 {{plan}} 美元。",
|
||||
"workspace.black.subscription.message": "您已订阅 OpenCode Black,费用为每月 ${{plan}}。",
|
||||
"workspace.black.subscription.manage": "管理订阅",
|
||||
"workspace.black.subscription.rollingUsage": "5小时使用",
|
||||
"workspace.black.subscription.weeklyUsage": "每周使用量",
|
||||
"workspace.black.subscription.resetsIn": "重置于",
|
||||
"workspace.black.subscription.useBalance": "达到使用限额后使用您的可用余额",
|
||||
"workspace.black.waitlist.title": "候补名单",
|
||||
"workspace.black.waitlist.joined": "您正在等待每月 ${{plan}} OpenCode 黑色计划。",
|
||||
"workspace.black.waitlist.ready": "我们已准备好让您加入每月 {{plan}} 美元的 OpenCode 黑色计划。",
|
||||
"workspace.black.waitlist.joined": "您已加入每月 ${{plan}} 的 OpenCode Black 方案候补名单。",
|
||||
"workspace.black.waitlist.ready": "我们已准备好将您加入每月 ${{plan}} 的 OpenCode Black 方案。",
|
||||
"workspace.black.waitlist.leave": "离开候补名单",
|
||||
"workspace.black.waitlist.leaving": "离开...",
|
||||
"workspace.black.waitlist.left": "左边",
|
||||
"workspace.black.waitlist.enroll": "注册",
|
||||
"workspace.black.waitlist.enrolling": "正在报名...",
|
||||
"workspace.black.waitlist.enrolled": "已注册",
|
||||
"workspace.black.waitlist.left": "已退出",
|
||||
"workspace.black.waitlist.enroll": "加入",
|
||||
"workspace.black.waitlist.enrolling": "加入中...",
|
||||
"workspace.black.waitlist.enrolled": "已加入",
|
||||
"workspace.black.waitlist.enrollNote": "单击“注册”后,您的订阅将立即开始,并且将从您的卡中扣费。",
|
||||
} satisfies Dict
|
||||
|
||||
@@ -293,9 +293,9 @@ export const dict = {
|
||||
"changelog.hero.subtitle": "OpenCode \u7684\u65b0\u66f4\u65b0\u8207\u6539\u5584",
|
||||
"changelog.empty": "\u627e\u4e0d\u5230\u66f4\u65b0\u65e5\u8a8c\u9805\u76ee\u3002",
|
||||
"changelog.viewJson": "\u6aa2\u8996 JSON",
|
||||
"workspace.nav.zen": "禪",
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.apiKeys": "API 鍵",
|
||||
"workspace.nav.members": "會員",
|
||||
"workspace.nav.members": "成員",
|
||||
"workspace.nav.billing": "計費",
|
||||
"workspace.nav.settings": "設定",
|
||||
"workspace.home.banner.beforeLink": "編碼代理的可靠優化模型。",
|
||||
@@ -310,26 +310,26 @@ export const dict = {
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"將 Zen 與任何編碼代理結合使用,並在需要時繼續將其他提供程序與 opencode 結合使用。",
|
||||
"workspace.newUser.copyApiKey": "複製 API 密鑰",
|
||||
"workspace.newUser.copyKey": "複製鑰匙",
|
||||
"workspace.newUser.copied": "複製了!",
|
||||
"workspace.newUser.copyKey": "複製密鑰",
|
||||
"workspace.newUser.copied": "已複製!",
|
||||
"workspace.newUser.step.enableBilling": "啟用計費",
|
||||
"workspace.newUser.step.login.before": "跑步",
|
||||
"workspace.newUser.step.login.before": "執行",
|
||||
"workspace.newUser.step.login.after": "並選擇 opencode",
|
||||
"workspace.newUser.step.pasteKey": "粘貼您的 API 密鑰",
|
||||
"workspace.newUser.step.models.before": "啟動 opencode 並運行",
|
||||
"workspace.newUser.step.models.after": "選擇型號",
|
||||
"workspace.models.title": "型號",
|
||||
"workspace.newUser.step.models.after": "選擇模型",
|
||||
"workspace.models.title": "模型",
|
||||
"workspace.models.subtitle.beforeLink": "管理工作區成員可以訪問哪些模型。",
|
||||
"workspace.models.table.model": "模型",
|
||||
"workspace.models.table.enabled": "啟用",
|
||||
"workspace.providers.title": "帶上你自己的鑰匙",
|
||||
"workspace.providers.title": "自帶密鑰",
|
||||
"workspace.providers.subtitle": "從 AI 提供商處配置您自己的 API 密鑰。",
|
||||
"workspace.providers.placeholder": "輸入 {{provider}} API 密鑰({{prefix}}...)",
|
||||
"workspace.providers.configure": "配置",
|
||||
"workspace.providers.edit": "編輯",
|
||||
"workspace.providers.delete": "刪除",
|
||||
"workspace.providers.saving": "保存...",
|
||||
"workspace.providers.save": "節省",
|
||||
"workspace.providers.save": "儲存",
|
||||
"workspace.providers.table.provider": "提供者",
|
||||
"workspace.providers.table.apiKey": "API 密鑰",
|
||||
"workspace.usage.title": "使用歷史",
|
||||
@@ -348,25 +348,25 @@ export const dict = {
|
||||
"workspace.usage.subscription": "訂閱 (${{amount}})",
|
||||
"workspace.cost.title": "成本",
|
||||
"workspace.cost.subtitle": "按型號細分的使用成本。",
|
||||
"workspace.cost.allModels": "所有型號",
|
||||
"workspace.cost.allKeys": "所有按鍵",
|
||||
"workspace.cost.allModels": "所有模型",
|
||||
"workspace.cost.allKeys": "所有密鑰",
|
||||
"workspace.cost.deletedSuffix": "(已刪除)",
|
||||
"workspace.cost.empty": "所選期間沒有可用的使用數據。",
|
||||
"workspace.cost.subscriptionShort": "子",
|
||||
"workspace.cost.subscriptionShort": "訂",
|
||||
"workspace.keys.title": "API 鍵",
|
||||
"workspace.keys.subtitle": "管理您的 API 密鑰以訪問 opencode 服務。",
|
||||
"workspace.keys.create": "創建 API 密鑰",
|
||||
"workspace.keys.placeholder": "輸入按鍵名稱",
|
||||
"workspace.keys.placeholder": "輸入密鑰名稱",
|
||||
"workspace.keys.empty": "創建 opencode 網關 API 密鑰",
|
||||
"workspace.keys.table.name": "姓名",
|
||||
"workspace.keys.table.key": "鑰匙",
|
||||
"workspace.keys.table.name": "名稱",
|
||||
"workspace.keys.table.key": "密鑰",
|
||||
"workspace.keys.table.createdBy": "創建者",
|
||||
"workspace.keys.table.lastUsed": "最後使用",
|
||||
"workspace.keys.copyApiKey": "複製 API 密鑰",
|
||||
"workspace.keys.delete": "刪除",
|
||||
"workspace.members.title": "會員",
|
||||
"workspace.members.title": "成員",
|
||||
"workspace.members.subtitle": "管理工作區成員及其權限。",
|
||||
"workspace.members.invite": "邀請會員",
|
||||
"workspace.members.invite": "邀請成員",
|
||||
"workspace.members.inviting": "邀請...",
|
||||
"workspace.members.beta.beforeLink": "測試期間,工作空間對團隊免費。",
|
||||
"workspace.members.form.invitee": "受邀者",
|
||||
@@ -379,11 +379,11 @@ export const dict = {
|
||||
"workspace.members.edit": "編輯",
|
||||
"workspace.members.delete": "刪除",
|
||||
"workspace.members.saving": "保存...",
|
||||
"workspace.members.save": "節省",
|
||||
"workspace.members.save": "儲存",
|
||||
"workspace.members.table.email": "電子郵件",
|
||||
"workspace.members.table.role": "角色",
|
||||
"workspace.members.table.monthLimit": "月份限制",
|
||||
"workspace.members.role.admin": "行政",
|
||||
"workspace.members.table.monthLimit": "月限額",
|
||||
"workspace.members.role.admin": "管理員",
|
||||
"workspace.members.role.adminDescription": "可以管理模型、成員和計費",
|
||||
"workspace.members.role.member": "成員",
|
||||
"workspace.members.role.memberDescription": "只能為自己生成 API 密鑰",
|
||||
@@ -392,7 +392,7 @@ export const dict = {
|
||||
"workspace.settings.workspaceName": "工作區名稱",
|
||||
"workspace.settings.defaultName": "預設",
|
||||
"workspace.settings.updating": "更新中...",
|
||||
"workspace.settings.save": "節省",
|
||||
"workspace.settings.save": "儲存",
|
||||
"workspace.settings.edit": "編輯",
|
||||
"workspace.billing.title": "計費",
|
||||
"workspace.billing.subtitle.beforeLink": "管理付款方式。",
|
||||
@@ -404,35 +404,35 @@ export const dict = {
|
||||
"workspace.billing.loading": "載入中...",
|
||||
"workspace.billing.addAction": "添加",
|
||||
"workspace.billing.addBalance": "添加餘額",
|
||||
"workspace.billing.linkedToStripe": "鏈接到條紋",
|
||||
"workspace.billing.linkedToStripe": "已連結 Stripe",
|
||||
"workspace.billing.manage": "管理",
|
||||
"workspace.billing.enable": "啟用計費",
|
||||
"workspace.monthlyLimit.title": "每月限額",
|
||||
"workspace.monthlyLimit.subtitle": "為您的帳戶設置每月使用限額。",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "環境...",
|
||||
"workspace.monthlyLimit.set": "放",
|
||||
"workspace.monthlyLimit.setting": "設定中...",
|
||||
"workspace.monthlyLimit.set": "設定",
|
||||
"workspace.monthlyLimit.edit": "編輯限制",
|
||||
"workspace.monthlyLimit.noLimit": "沒有設置使用限制。",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "當前使用情況為",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "是 $",
|
||||
"workspace.reload.title": "自動重新加載",
|
||||
"workspace.reload.disabled.before": "自動重新加載是",
|
||||
"workspace.reload.disabled.state": "殘疾人",
|
||||
"workspace.reload.disabled.after": "啟用餘額不足時自動充值。",
|
||||
"workspace.reload.enabled.before": "自動重新加載是",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "當前",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $",
|
||||
"workspace.reload.title": "自動儲值",
|
||||
"workspace.reload.disabled.before": "自動儲值已",
|
||||
"workspace.reload.disabled.state": "停用",
|
||||
"workspace.reload.disabled.after": "啟用後會在餘額偏低時自動儲值。",
|
||||
"workspace.reload.enabled.before": "自動儲值已",
|
||||
"workspace.reload.enabled.state": "已啟用",
|
||||
"workspace.reload.enabled.middle": "我們將重新加載",
|
||||
"workspace.reload.processingFee": "加工費",
|
||||
"workspace.reload.enabled.middle": "我們將自動儲值",
|
||||
"workspace.reload.processingFee": "手續費",
|
||||
"workspace.reload.enabled.after": "當餘額達到",
|
||||
"workspace.reload.edit": "編輯",
|
||||
"workspace.reload.enable": "使能夠",
|
||||
"workspace.reload.enableAutoReload": "啟用自動重新加載",
|
||||
"workspace.reload.reloadAmount": "重新加載 $",
|
||||
"workspace.reload.enable": "啟用",
|
||||
"workspace.reload.enableAutoReload": "啟用自動儲值",
|
||||
"workspace.reload.reloadAmount": "儲值 $",
|
||||
"workspace.reload.whenBalanceReaches": "當餘額達到 $",
|
||||
"workspace.reload.saving": "保存...",
|
||||
"workspace.reload.save": "節省",
|
||||
"workspace.reload.failedAt": "重新加載失敗於",
|
||||
"workspace.reload.save": "儲存",
|
||||
"workspace.reload.failedAt": "儲值失敗於",
|
||||
"workspace.reload.reason": "原因:",
|
||||
"workspace.reload.updatePaymentMethod": "請更新您的付款方式並重試。",
|
||||
"workspace.reload.retrying": "正在重試...",
|
||||
@@ -441,11 +441,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "最近的付款交易。",
|
||||
"workspace.payments.table.date": "日期",
|
||||
"workspace.payments.table.paymentId": "付款ID",
|
||||
"workspace.payments.table.amount": "數量",
|
||||
"workspace.payments.table.amount": "金額",
|
||||
"workspace.payments.table.receipt": "收據",
|
||||
"workspace.payments.type.credit": "信用",
|
||||
"workspace.payments.type.subscription": "訂閱",
|
||||
"workspace.payments.view": "看法",
|
||||
"workspace.payments.view": "查看",
|
||||
"workspace.black.loading": "載入中...",
|
||||
"workspace.black.time.day": "天",
|
||||
"workspace.black.time.days": "天",
|
||||
@@ -455,20 +455,20 @@ export const dict = {
|
||||
"workspace.black.time.minutes": "分鐘",
|
||||
"workspace.black.time.fewSeconds": "幾秒鐘",
|
||||
"workspace.black.subscription.title": "訂閱",
|
||||
"workspace.black.subscription.message": "您已訂閱 OpenCode Black,每月費用為 {{plan}} 美元。",
|
||||
"workspace.black.subscription.message": "您已訂閱 OpenCode Black,費用為每月 ${{plan}}。",
|
||||
"workspace.black.subscription.manage": "管理訂閱",
|
||||
"workspace.black.subscription.rollingUsage": "5小時使用",
|
||||
"workspace.black.subscription.weeklyUsage": "每週使用量",
|
||||
"workspace.black.subscription.resetsIn": "重置於",
|
||||
"workspace.black.subscription.useBalance": "達到使用限額後使用您的可用餘額",
|
||||
"workspace.black.waitlist.title": "候補名單",
|
||||
"workspace.black.waitlist.joined": "您正在等待每月 ${{plan}} OpenCode 黑色計劃。",
|
||||
"workspace.black.waitlist.ready": "我們已準備好讓您加入每月 {{plan}} 美元的 OpenCode 黑色計劃。",
|
||||
"workspace.black.waitlist.joined": "您已加入每月 ${{plan}} 的 OpenCode Black 方案候補名單。",
|
||||
"workspace.black.waitlist.ready": "我們已準備好將您加入每月 ${{plan}} 的 OpenCode Black 方案。",
|
||||
"workspace.black.waitlist.leave": "離開候補名單",
|
||||
"workspace.black.waitlist.leaving": "離開...",
|
||||
"workspace.black.waitlist.left": "左邊",
|
||||
"workspace.black.waitlist.enroll": "註冊",
|
||||
"workspace.black.waitlist.enrolling": "正在報名...",
|
||||
"workspace.black.waitlist.enrolled": "已註冊",
|
||||
"workspace.black.waitlist.left": "已退出",
|
||||
"workspace.black.waitlist.enroll": "加入",
|
||||
"workspace.black.waitlist.enrolling": "加入中...",
|
||||
"workspace.black.waitlist.enrolled": "已加入",
|
||||
"workspace.black.waitlist.enrollNote": "單擊“註冊”後,您的訂閱將立即開始,並且將從您的卡中扣費。",
|
||||
} satisfies Dict
|
||||
|
||||
@@ -68,6 +68,82 @@ const TAG = {
|
||||
tr: "tr",
|
||||
} satisfies Record<Locale, string>
|
||||
|
||||
const DOCS = {
|
||||
en: "root",
|
||||
zh: "zh-cn",
|
||||
zht: "zh-tw",
|
||||
ko: "ko",
|
||||
de: "de",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
it: "it",
|
||||
da: "da",
|
||||
ja: "ja",
|
||||
pl: "pl",
|
||||
ru: "ru",
|
||||
ar: "ar",
|
||||
no: "nb",
|
||||
br: "pt-br",
|
||||
th: "th",
|
||||
tr: "tr",
|
||||
} satisfies Record<Locale, string>
|
||||
|
||||
const DOCS_SEGMENT = new Set([
|
||||
"ar",
|
||||
"bs",
|
||||
"da",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"nb",
|
||||
"pl",
|
||||
"pt-br",
|
||||
"ru",
|
||||
"th",
|
||||
"tr",
|
||||
"zh-cn",
|
||||
"zh-tw",
|
||||
])
|
||||
|
||||
function suffix(pathname: string) {
|
||||
const index = pathname.search(/[?#]/)
|
||||
if (index === -1) {
|
||||
return {
|
||||
path: fix(pathname),
|
||||
suffix: "",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
path: fix(pathname.slice(0, index)),
|
||||
suffix: pathname.slice(index),
|
||||
}
|
||||
}
|
||||
|
||||
export function docs(locale: Locale, pathname: string) {
|
||||
const value = DOCS[locale]
|
||||
const next = suffix(pathname)
|
||||
if (next.path !== "/docs" && next.path !== "/docs/" && !next.path.startsWith("/docs/")) {
|
||||
return `${next.path}${next.suffix}`
|
||||
}
|
||||
|
||||
if (value === "root") return `${next.path}${next.suffix}`
|
||||
|
||||
if (next.path === "/docs") return `/docs/${value}${next.suffix}`
|
||||
if (next.path === "/docs/") return `/docs/${value}/${next.suffix}`
|
||||
|
||||
const head = next.path.slice("/docs/".length).split("/")[0] ?? ""
|
||||
if (!head) return `/docs/${value}/${next.suffix}`
|
||||
if (DOCS_SEGMENT.has(head)) return `${next.path}${next.suffix}`
|
||||
if (head.startsWith("_")) return `${next.path}${next.suffix}`
|
||||
if (head.includes(".")) return `${next.path}${next.suffix}`
|
||||
|
||||
return `/docs/${value}${next.path.slice("/docs".length)}${next.suffix}`
|
||||
}
|
||||
|
||||
export function parseLocale(value: unknown): Locale | null {
|
||||
if (typeof value !== "string") return null
|
||||
if ((LOCALES as readonly string[]).includes(value)) return value as Locale
|
||||
@@ -90,7 +166,7 @@ export function strip(pathname: string) {
|
||||
|
||||
export function route(locale: Locale, pathname: string) {
|
||||
const next = strip(pathname)
|
||||
if (next.startsWith("/docs")) return next
|
||||
if (next.startsWith("/docs")) return docs(locale, next)
|
||||
if (next.startsWith("/auth")) return next
|
||||
if (next.startsWith("/workspace")) return next
|
||||
if (locale === "en") return next
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { docs, localeFromRequest, tag } from "~/lib/language"
|
||||
|
||||
async function handler(evt: APIEvent) {
|
||||
const req = evt.request.clone()
|
||||
const url = new URL(req.url)
|
||||
const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
|
||||
const locale = localeFromRequest(req)
|
||||
const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai"
|
||||
const targetUrl = `https://${host}${docs(locale, url.pathname)}${url.search}`
|
||||
|
||||
const headers = new Headers(req.headers)
|
||||
const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
|
||||
if (locale) headers.set("accept-language", tag(locale))
|
||||
headers.set("accept-language", tag(locale))
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: req.method,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { docs, localeFromRequest, tag } from "~/lib/language"
|
||||
|
||||
async function handler(evt: APIEvent) {
|
||||
const req = evt.request.clone()
|
||||
const url = new URL(req.url)
|
||||
const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
|
||||
const locale = localeFromRequest(req)
|
||||
const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai"
|
||||
const targetUrl = `https://${host}${docs(locale, url.pathname)}${url.search}`
|
||||
|
||||
const headers = new Headers(req.headers)
|
||||
const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
|
||||
if (locale) headers.set("accept-language", tag(locale))
|
||||
headers.set("accept-language", tag(locale))
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: req.method,
|
||||
|
||||
@@ -294,7 +294,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>VS Code</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -318,7 +318,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>Cursor</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -335,7 +335,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>Zed</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -352,7 +352,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>Windsurf</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -369,7 +369,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>VSCodium</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -393,7 +393,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>GitHub</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/github/" data-component="action-button">
|
||||
<a href={language.route("/docs/github/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -410,7 +410,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>GitLab</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/gitlab/" data-component="action-button">
|
||||
<a href={language.route("/docs/gitlab/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { docs, localeFromRequest, tag } from "~/lib/language"
|
||||
|
||||
async function handler(evt: APIEvent) {
|
||||
const req = evt.request.clone()
|
||||
const url = new URL(req.url)
|
||||
const targetUrl = `https://docs.opencode.ai/docs${url.pathname}${url.search}`
|
||||
const locale = localeFromRequest(req)
|
||||
const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai"
|
||||
const targetUrl = `https://${host}${docs(locale, `/docs${url.pathname}`)}${url.search}`
|
||||
|
||||
const headers = new Headers(req.headers)
|
||||
const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
|
||||
if (locale) headers.set("accept-language", tag(locale))
|
||||
headers.set("accept-language", tag(locale))
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: req.method,
|
||||
|
||||
@@ -9,10 +9,12 @@ import { GraphSection } from "./graph-section"
|
||||
import { IconLogo } from "~/component/icon"
|
||||
import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const userInfo = createAsync(() => querySessionInfo(params.id!))
|
||||
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
|
||||
const checkoutAction = useAction(createCheckoutUrl)
|
||||
@@ -38,7 +40,7 @@ export default function () {
|
||||
<p>
|
||||
<span>
|
||||
{i18n.t("workspace.home.banner.beforeLink")}{" "}
|
||||
<a target="_blank" href="/docs/zen">
|
||||
<a target="_blank" href={language.route("/docs/zen")}>
|
||||
{i18n.t("common.learnMore")}
|
||||
</a>
|
||||
.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export class AuthError extends Error {}
|
||||
export class CreditsError extends Error {}
|
||||
export class MonthlyLimitError extends Error {}
|
||||
export class SubscriptionError extends Error {
|
||||
export class UserLimitError extends Error {}
|
||||
export class ModelError extends Error {}
|
||||
export class FreeUsageLimitError extends Error {}
|
||||
export class SubscriptionUsageLimitError extends Error {
|
||||
retryAfter?: number
|
||||
constructor(message: string, retryAfter?: number) {
|
||||
super(message)
|
||||
this.retryAfter = retryAfter
|
||||
}
|
||||
}
|
||||
export class UserLimitError extends Error {}
|
||||
export class ModelError extends Error {}
|
||||
export class RateLimitError extends Error {}
|
||||
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
AuthError,
|
||||
CreditsError,
|
||||
MonthlyLimitError,
|
||||
SubscriptionError,
|
||||
UserLimitError,
|
||||
ModelError,
|
||||
RateLimitError,
|
||||
FreeUsageLimitError,
|
||||
SubscriptionUsageLimitError,
|
||||
} from "./error"
|
||||
import { createBodyConverter, createStreamPartConverter, createResponseConverter, UsageInfo } from "./provider/provider"
|
||||
import { anthropicHelper } from "./provider/anthropic"
|
||||
@@ -52,7 +52,8 @@ export async function handler(
|
||||
type ModelInfo = Awaited<ReturnType<typeof validateModel>>
|
||||
type ProviderInfo = Awaited<ReturnType<typeof selectProvider>>
|
||||
|
||||
const MAX_RETRIES = 3
|
||||
const MAX_FAILOVER_RETRIES = 3
|
||||
const MAX_429_RETRIES = 3
|
||||
const FREE_WORKSPACES = [
|
||||
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
|
||||
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
|
||||
@@ -111,7 +112,7 @@ export async function handler(
|
||||
)
|
||||
logger.debug("REQUEST URL: " + reqUrl)
|
||||
logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...")
|
||||
const res = await fetch(reqUrl, {
|
||||
const res = await fetchWith429Retry(reqUrl, {
|
||||
method: "POST",
|
||||
headers: (() => {
|
||||
const headers = new Headers(input.request.headers)
|
||||
@@ -304,9 +305,9 @@ export async function handler(
|
||||
{ status: 401 },
|
||||
)
|
||||
|
||||
if (error instanceof RateLimitError || error instanceof SubscriptionError) {
|
||||
if (error instanceof FreeUsageLimitError || error instanceof SubscriptionUsageLimitError) {
|
||||
const headers = new Headers()
|
||||
if (error instanceof SubscriptionError && error.retryAfter) {
|
||||
if (error instanceof SubscriptionUsageLimitError && error.retryAfter) {
|
||||
headers.set("retry-after", String(error.retryAfter))
|
||||
}
|
||||
return new Response(
|
||||
@@ -369,7 +370,7 @@ export async function handler(
|
||||
if (provider) return provider
|
||||
}
|
||||
|
||||
if (retry.retryCount === MAX_RETRIES) {
|
||||
if (retry.retryCount === MAX_FAILOVER_RETRIES) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
}
|
||||
|
||||
@@ -520,7 +521,7 @@ export async function handler(
|
||||
timeUpdated: sub.timeFixedUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionError(
|
||||
throw new SubscriptionUsageLimitError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
@@ -534,7 +535,7 @@ export async function handler(
|
||||
timeUpdated: sub.timeRollingUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionError(
|
||||
throw new SubscriptionUsageLimitError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
@@ -597,6 +598,15 @@ export async function handler(
|
||||
providerInfo.apiKey = authInfo.provider.credentials
|
||||
}
|
||||
|
||||
async function fetchWith429Retry(url: string, options: RequestInit, retry = { count: 0 }) {
|
||||
const res = await fetch(url, options)
|
||||
if (res.status === 429 && retry.count < MAX_429_RETRIES) {
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, retry.count) * 500))
|
||||
return fetchWith429Retry(url, options, { count: retry.count + 1 })
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function trackUsage(
|
||||
authInfo: AuthInfo,
|
||||
modelInfo: ModelInfo,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Database, eq, and, sql, inArray } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { RateLimitError } from "./error"
|
||||
import { FreeUsageLimitError } from "./error"
|
||||
import { logger } from "./logger"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
|
||||
@@ -34,7 +34,7 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s
|
||||
)
|
||||
const total = rows.reduce((sum, r) => sum + r.count, 0)
|
||||
logger.debug(`rate limit total: ${total}`)
|
||||
if (total >= limitValue) throw new RateLimitError(`Rate limit exceeded. Please try again later.`)
|
||||
if (total >= limitValue) throw new FreeUsageLimitError(`Rate limit exceeded. Please try again later.`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@planetscale/database": "1.19.0",
|
||||
"aws4fetch": "1.0.20",
|
||||
"drizzle-orm": "0.41.0",
|
||||
"drizzle-orm": "catalog:",
|
||||
"postgres": "3.4.7",
|
||||
"stripe": "18.0.0",
|
||||
"ulid": "catalog:",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.0",
|
||||
"@types/node": "catalog:",
|
||||
"drizzle-kit": "0.30.5",
|
||||
"drizzle-kit": "catalog:",
|
||||
"mysql2": "3.14.4",
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from "drizzle-orm"
|
||||
import { Client } from "@planetscale/database"
|
||||
|
||||
import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core"
|
||||
import type { ExtractTablesWithRelations } from "drizzle-orm"
|
||||
import type { PlanetScalePreparedQueryHKT, PlanetscaleQueryResultHKT } from "drizzle-orm/planetscale-serverless"
|
||||
import { Context } from "../context"
|
||||
import { memo } from "../util/memo"
|
||||
@@ -14,7 +13,7 @@ export namespace Database {
|
||||
PlanetscaleQueryResultHKT,
|
||||
PlanetScalePreparedQueryHKT,
|
||||
Record<string, never>,
|
||||
ExtractTablesWithRelations<Record<string, never>>
|
||||
any
|
||||
>
|
||||
|
||||
const client = memo(() => {
|
||||
@@ -23,7 +22,7 @@ export namespace Database {
|
||||
username: Resource.Database.username,
|
||||
password: Resource.Database.password,
|
||||
})
|
||||
const db = drizzle(result, {})
|
||||
const db = drizzle({ client: result })
|
||||
return db
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ mod constants;
|
||||
#[cfg(windows)]
|
||||
mod job_object;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux_display;
|
||||
pub mod linux_display;
|
||||
mod markdown;
|
||||
mod server;
|
||||
mod window_customizer;
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
// borrowed from https://github.com/skyline69/balatro-mod-manager
|
||||
#[cfg(target_os = "linux")]
|
||||
mod display;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn configure_display_backend() -> Option<String> {
|
||||
use std::env;
|
||||
@@ -26,7 +23,7 @@ fn configure_display_backend() -> Option<String> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prefer_wayland = display::read_wayland().unwrap_or(false);
|
||||
let prefer_wayland = opencode_lib::linux_display::read_wayland().unwrap_or(false);
|
||||
let allow_wayland = prefer_wayland
|
||||
|| matches!(
|
||||
env::var("OC_ALLOW_WAYLAND"),
|
||||
|
||||
@@ -116,6 +116,15 @@ function parseRecord(value: unknown) {
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
function parseStored(value: unknown) {
|
||||
if (typeof value !== "string") return value
|
||||
try {
|
||||
return JSON.parse(value) as unknown
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function pickLocale(value: unknown): Locale | null {
|
||||
const direct = parseLocale(value)
|
||||
if (direct) return direct
|
||||
@@ -169,7 +178,7 @@ export function initI18n(): Promise<Locale> {
|
||||
if (!store) return state.locale
|
||||
|
||||
const raw = await store.get("language").catch(() => null)
|
||||
const value = typeof raw === "string" ? JSON.parse(raw) : raw
|
||||
const value = parseStored(raw)
|
||||
const next = pickLocale(value) ?? state.locale
|
||||
|
||||
state.locale = next
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
# opencode agent guidelines
|
||||
# opencode database guide
|
||||
|
||||
## Build/Test Commands
|
||||
## Database
|
||||
|
||||
- **Install**: `bun install`
|
||||
- **Run**: `bun run --conditions=browser ./src/index.ts`
|
||||
- **Typecheck**: `bun run typecheck` (npm run typecheck)
|
||||
- **Test**: `bun test` (runs all tests)
|
||||
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Runtime**: Bun with TypeScript ESM modules
|
||||
- **Imports**: Use relative imports for local modules, named imports preferred
|
||||
- **Types**: Zod schemas for validation, TypeScript interfaces for structure
|
||||
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
|
||||
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
|
||||
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Tools**: Implement `Tool.Info` interface with `execute()` method
|
||||
- **Context**: Pass `sessionID` in tool context, use `App.provide()` for DI
|
||||
- **Validation**: All inputs validated with Zod schemas
|
||||
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
||||
- **Storage**: Use `Storage` namespace for persistence
|
||||
- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files.
|
||||
- **Schema**: Drizzle schema lives in `src/**/*.sql.ts`.
|
||||
- **Naming**: tables and columns use snake*case; join columns are `<entity>_id`; indexes are `<table>*<column>\_idx`.
|
||||
- **Migrations**: generated by Drizzle Kit using `drizzle.config.ts` (schema: `./src/**/*.sql.ts`, output: `./migration`).
|
||||
- **Command**: `bun run db generate --name <slug>`.
|
||||
- **Output**: creates `migration/<timestamp>_<slug>/migration.sql` and `snapshot.json`.
|
||||
- **Tests**: migration tests should read the per-folder layout (no `_journal.json`).
|
||||
|
||||
10
packages/opencode/drizzle.config.ts
Normal file
10
packages/opencode/drizzle.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "drizzle-kit"
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
schema: "./src/**/*.sql.ts",
|
||||
out: "./migration",
|
||||
dbCredentials: {
|
||||
url: "/home/thdxr/.local/share/opencode/opencode.db",
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
CREATE TABLE `project` (
|
||||
`id` text PRIMARY KEY,
|
||||
`worktree` text NOT NULL,
|
||||
`vcs` text,
|
||||
`name` text,
|
||||
`icon_url` text,
|
||||
`icon_color` text,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`time_initialized` integer,
|
||||
`sandboxes` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `message` (
|
||||
`id` text PRIMARY KEY,
|
||||
`session_id` text NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `part` (
|
||||
`id` text PRIMARY KEY,
|
||||
`message_id` text NOT NULL,
|
||||
`session_id` text NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `permission` (
|
||||
`project_id` text PRIMARY KEY,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `session` (
|
||||
`id` text PRIMARY KEY,
|
||||
`project_id` text NOT NULL,
|
||||
`parent_id` text,
|
||||
`slug` text NOT NULL,
|
||||
`directory` text NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`version` text NOT NULL,
|
||||
`share_url` text,
|
||||
`summary_additions` integer,
|
||||
`summary_deletions` integer,
|
||||
`summary_files` integer,
|
||||
`summary_diffs` text,
|
||||
`revert` text,
|
||||
`permission` text,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`time_compacting` integer,
|
||||
`time_archived` integer,
|
||||
CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `todo` (
|
||||
`session_id` text NOT NULL,
|
||||
`content` text NOT NULL,
|
||||
`status` text NOT NULL,
|
||||
`priority` text NOT NULL,
|
||||
`position` integer NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `position`),
|
||||
CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `session_share` (
|
||||
`session_id` text PRIMARY KEY,
|
||||
`id` text NOT NULL,
|
||||
`secret` text NOT NULL,
|
||||
`url` text NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint
|
||||
CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint
|
||||
CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint
|
||||
CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint
|
||||
CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint
|
||||
CREATE INDEX `todo_session_idx` ON `todo` (`session_id`);
|
||||
@@ -0,0 +1,796 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "068758ed-a97a-46f6-8a59-6c639ae7c20c",
|
||||
"prevIds": ["00000000-0000-0000-0000-000000000000"],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "project",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "part",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "todo",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_share",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "worktree",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "vcs",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_color",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_initialized",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "sandboxes",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "message_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "parent_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "slug",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "title",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "version",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "share_url",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_additions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_deletions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_files",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_diffs",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "revert",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "permission",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_compacting",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_archived",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "content",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "status",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "priority",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "position",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "secret",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": ["message_id"],
|
||||
"tableTo": "message",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_part_message_id_message_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_permission_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_todo_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_share_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id", "position"],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "message_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "message_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_message_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "project_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_project_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "parent_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_parent_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "todo_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "todo"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
@@ -15,7 +15,8 @@
|
||||
"lint": "echo 'Running lint checks...' && bun test --coverage",
|
||||
"format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
|
||||
"docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
|
||||
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'"
|
||||
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'",
|
||||
"db": "bun drizzle-kit"
|
||||
},
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode"
|
||||
@@ -42,6 +43,8 @@
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "1.0.0-beta.12-a5629fb",
|
||||
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
||||
"typescript": "catalog:",
|
||||
"vscode-languageserver-types": "3.17.5",
|
||||
"why-is-node-running": "3.2.2",
|
||||
@@ -100,6 +103,7 @@
|
||||
"clipboardy": "4.0.0",
|
||||
"decimal.js": "10.5.0",
|
||||
"diff": "catalog:",
|
||||
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gray-matter": "4.0.3",
|
||||
"hono": "catalog:",
|
||||
@@ -122,5 +126,8 @@
|
||||
"yargs": "18.0.0",
|
||||
"zod": "catalog:",
|
||||
"zod-to-json-schema": "3.24.5"
|
||||
},
|
||||
"overrides": {
|
||||
"drizzle-orm": "1.0.0-beta.12-a5629fb"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,32 @@ await Bun.write(
|
||||
)
|
||||
console.log("Generated models-snapshot.ts")
|
||||
|
||||
// Load migrations from migration directories
|
||||
const migrationDirs = (await fs.promises.readdir(path.join(dir, "migration"), { withFileTypes: true }))
|
||||
.filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
|
||||
.map((entry) => entry.name)
|
||||
.sort()
|
||||
|
||||
const migrations = await Promise.all(
|
||||
migrationDirs.map(async (name) => {
|
||||
const file = path.join(dir, "migration", name, "migration.sql")
|
||||
const sql = await Bun.file(file).text()
|
||||
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
|
||||
const timestamp = match
|
||||
? Date.UTC(
|
||||
Number(match[1]),
|
||||
Number(match[2]) - 1,
|
||||
Number(match[3]),
|
||||
Number(match[4]),
|
||||
Number(match[5]),
|
||||
Number(match[6]),
|
||||
)
|
||||
: 0
|
||||
return { sql, timestamp }
|
||||
}),
|
||||
)
|
||||
console.log(`Loaded ${migrations.length} migrations`)
|
||||
|
||||
const singleFlag = process.argv.includes("--single")
|
||||
const baselineFlag = process.argv.includes("--baseline")
|
||||
const skipInstall = process.argv.includes("--skip-install")
|
||||
@@ -156,6 +182,7 @@ for (const item of targets) {
|
||||
entrypoints: ["./src/index.ts", parserWorker, workerPath],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
||||
OPENCODE_WORKER_PATH: workerPath,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
|
||||
16
packages/opencode/script/check-migrations.ts
Normal file
16
packages/opencode/script/check-migrations.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
|
||||
// drizzle-kit check compares schema to migrations, exits non-zero if drift
|
||||
const result = await $`bun drizzle-kit check`.quiet().nothrow()
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
console.error("Schema has changes not captured in migrations!")
|
||||
console.error("Run: bun drizzle-kit generate")
|
||||
console.error("")
|
||||
console.error(result.stderr.toString())
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log("Migrations are up to date")
|
||||
@@ -228,8 +228,8 @@ export namespace ACP {
|
||||
const metadata = permission.metadata || {}
|
||||
const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
|
||||
const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
|
||||
|
||||
const content = await Bun.file(filepath).text()
|
||||
const file = Bun.file(filepath)
|
||||
const content = (await file.exists()) ? await file.text() : ""
|
||||
const newContent = getNewContent(content, diff)
|
||||
|
||||
if (newContent) {
|
||||
@@ -435,46 +435,68 @@ export namespace ACP {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (part.type === "text") {
|
||||
const delta = props.delta
|
||||
if (delta && part.ignored !== true) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: delta,
|
||||
},
|
||||
case "message.part.delta": {
|
||||
const props = event.properties
|
||||
const session = this.sessionManager.tryGet(props.sessionID)
|
||||
if (!session) return
|
||||
const sessionId = session.id
|
||||
|
||||
const message = await this.sdk.session
|
||||
.message(
|
||||
{
|
||||
sessionID: props.sessionID,
|
||||
messageID: props.messageID,
|
||||
directory: session.cwd,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.then((x) => x.data)
|
||||
.catch((error) => {
|
||||
log.error("unexpected error when fetching message", { error })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!message || message.info.role !== "assistant") return
|
||||
|
||||
const part = message.parts.find((p) => p.id === props.partID)
|
||||
if (!part) return
|
||||
|
||||
if (part.type === "text" && props.field === "text" && part.ignored !== true) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: props.delta,
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send text to ACP", { error })
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send text delta to ACP", { error })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (part.type === "reasoning") {
|
||||
const delta = props.delta
|
||||
if (delta) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_thought_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: delta,
|
||||
},
|
||||
if (part.type === "reasoning" && props.field === "text") {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_thought_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: props.delta,
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send reasoning to ACP", { error })
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send reasoning delta to ACP", { error })
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -184,18 +184,6 @@ export namespace Agent {
|
||||
),
|
||||
prompt: PROMPT_TITLE,
|
||||
},
|
||||
handoff: {
|
||||
name: "handoff",
|
||||
mode: "primary",
|
||||
options: {},
|
||||
native: true,
|
||||
hidden: true,
|
||||
temperature: 0.5,
|
||||
permission: PermissionNext.fromConfig({
|
||||
"*": "allow",
|
||||
}),
|
||||
prompt: "none",
|
||||
},
|
||||
summary: {
|
||||
name: "summary",
|
||||
mode: "primary",
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
|
||||
import { Session } from "../../session"
|
||||
import { cmd } from "./cmd"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Storage } from "../../storage/storage"
|
||||
import { Database } from "../../storage/db"
|
||||
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { ShareNext } from "../../share/share-next"
|
||||
import { EOL } from "os"
|
||||
@@ -130,13 +131,35 @@ export const ImportCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
await Storage.write(["session", Instance.project.id, exportData.info.id], exportData.info)
|
||||
Database.use((db) => db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run())
|
||||
|
||||
for (const msg of exportData.messages) {
|
||||
await Storage.write(["message", exportData.info.id, msg.info.id], msg.info)
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(MessageTable)
|
||||
.values({
|
||||
id: msg.info.id,
|
||||
session_id: exportData.info.id,
|
||||
time_created: msg.info.time?.created ?? Date.now(),
|
||||
data: msg.info,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run(),
|
||||
)
|
||||
|
||||
for (const part of msg.parts) {
|
||||
await Storage.write(["part", msg.info.id, part.id], part)
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(PartTable)
|
||||
.values({
|
||||
id: part.id,
|
||||
message_id: msg.info.id,
|
||||
session_id: exportData.info.id,
|
||||
data: part,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import type { Argv } from "yargs"
|
||||
import { cmd } from "./cmd"
|
||||
import { Session } from "../../session"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Storage } from "../../storage/storage"
|
||||
import { Database } from "../../storage/db"
|
||||
import { SessionTable } from "../../session/session.sql"
|
||||
import { Project } from "../../project/project"
|
||||
import { Instance } from "../../project/instance"
|
||||
|
||||
@@ -87,25 +88,8 @@ async function getCurrentProject(): Promise<Project.Info> {
|
||||
}
|
||||
|
||||
async function getAllSessions(): Promise<Session.Info[]> {
|
||||
const sessions: Session.Info[] = []
|
||||
|
||||
const projectKeys = await Storage.list(["project"])
|
||||
const projects = await Promise.all(projectKeys.map((key) => Storage.read<Project.Info>(key)))
|
||||
|
||||
for (const project of projects) {
|
||||
if (!project) continue
|
||||
|
||||
const sessionKeys = await Storage.list(["session", project.id])
|
||||
const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key)))
|
||||
|
||||
for (const session of projectSessions) {
|
||||
if (session) {
|
||||
sessions.push(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sessions
|
||||
const rows = Database.use((db) => db.select().from(SessionTable).all())
|
||||
return rows.map((row) => Session.fromRow(row))
|
||||
}
|
||||
|
||||
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
|
||||
|
||||
@@ -83,7 +83,6 @@ function init() {
|
||||
},
|
||||
slashes() {
|
||||
return visibleOptions().flatMap((option) => {
|
||||
if (option.disabled) return []
|
||||
const slash = option.slash
|
||||
if (!slash) return []
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createMemo, createSignal } from "solid-js"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
|
||||
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
@@ -20,96 +20,51 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const keybind = useKeybind()
|
||||
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
|
||||
const [query, setQuery] = createSignal("")
|
||||
|
||||
const connected = useConnected()
|
||||
const providers = createDialogProviderOptions()
|
||||
|
||||
const showExtra = createMemo(() => {
|
||||
if (!connected()) return false
|
||||
if (props.providerID) return false
|
||||
return true
|
||||
})
|
||||
const showExtra = createMemo(() => connected() && !props.providerID)
|
||||
|
||||
const options = createMemo(() => {
|
||||
const q = query()
|
||||
const needle = q.trim()
|
||||
const needle = query().trim()
|
||||
const showSections = showExtra() && needle.length === 0
|
||||
const favorites = connected() ? local.model.favorite() : []
|
||||
const recents = local.model.recent()
|
||||
|
||||
const recentList = showSections
|
||||
? recents.filter(
|
||||
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
|
||||
)
|
||||
: []
|
||||
|
||||
const favoriteOptions = showSections
|
||||
? favorites.flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
||||
if (!provider) return []
|
||||
const model = provider.models[item.modelID]
|
||||
if (!model) return []
|
||||
return [
|
||||
{
|
||||
key: item,
|
||||
value: {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category: "Favorites",
|
||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
function toOptions(items: typeof favorites, category: string) {
|
||||
if (!showSections) return []
|
||||
return items.flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
||||
if (!provider) return []
|
||||
const model = provider.models[item.modelID]
|
||||
if (!model) return []
|
||||
return [
|
||||
{
|
||||
key: item,
|
||||
value: { providerID: provider.id, modelID: model.id },
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category,
|
||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true })
|
||||
},
|
||||
]
|
||||
})
|
||||
: []
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const recentOptions = showSections
|
||||
? recentList.flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
||||
if (!provider) return []
|
||||
const model = provider.models[item.modelID]
|
||||
if (!model) return []
|
||||
return [
|
||||
{
|
||||
key: item,
|
||||
value: {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category: "Recent",
|
||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
: []
|
||||
const favoriteOptions = toOptions(favorites, "Favorites")
|
||||
const recentOptions = toOptions(
|
||||
recents.filter(
|
||||
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
|
||||
),
|
||||
"Recent",
|
||||
)
|
||||
|
||||
const providerOptions = pipe(
|
||||
sync.data.provider,
|
||||
@@ -123,45 +78,26 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
entries(),
|
||||
filter(([_, info]) => info.status !== "deprecated"),
|
||||
filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
|
||||
map(([model, info]) => {
|
||||
const value = {
|
||||
providerID: provider.id,
|
||||
modelID: model,
|
||||
}
|
||||
return {
|
||||
value,
|
||||
title: info.name ?? model,
|
||||
description: favorites.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
? "(Favorite)"
|
||||
: undefined,
|
||||
category: connected() ? provider.name : undefined,
|
||||
disabled: provider.id === "opencode" && model.includes("-nano"),
|
||||
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect() {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
}
|
||||
}),
|
||||
map(([model, info]) => ({
|
||||
value: { providerID: provider.id, modelID: model },
|
||||
title: info.name ?? model,
|
||||
description: favorites.some((item) => item.providerID === provider.id && item.modelID === model)
|
||||
? "(Favorite)"
|
||||
: undefined,
|
||||
category: connected() ? provider.name : undefined,
|
||||
disabled: provider.id === "opencode" && model.includes("-nano"),
|
||||
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect() {
|
||||
dialog.clear()
|
||||
local.model.set({ providerID: provider.id, modelID: model }, { recent: true })
|
||||
},
|
||||
})),
|
||||
filter((x) => {
|
||||
if (!showSections) return true
|
||||
const value = x.value
|
||||
const inFavorites = favorites.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inFavorites) return false
|
||||
const inRecents = recents.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inRecents) return false
|
||||
if (favorites.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
|
||||
return false
|
||||
if (recents.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
|
||||
return false
|
||||
return true
|
||||
}),
|
||||
sortBy(
|
||||
@@ -175,21 +111,19 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
const popularProviders = !connected()
|
||||
? pipe(
|
||||
providers(),
|
||||
map((option) => {
|
||||
return {
|
||||
...option,
|
||||
category: "Popular providers",
|
||||
}
|
||||
}),
|
||||
map((option) => ({
|
||||
...option,
|
||||
category: "Popular providers",
|
||||
})),
|
||||
take(6),
|
||||
)
|
||||
: []
|
||||
|
||||
// Search shows a single merged list (favorites inline)
|
||||
if (needle) {
|
||||
const filteredProviders = fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
|
||||
const filteredPopular = fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj)
|
||||
return [...filteredProviders, ...filteredPopular]
|
||||
return [
|
||||
...fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj),
|
||||
...fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj),
|
||||
]
|
||||
}
|
||||
|
||||
return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
|
||||
@@ -199,13 +133,11 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
|
||||
)
|
||||
|
||||
const title = createMemo(() => {
|
||||
if (provider()) return provider()!.name
|
||||
return "Select model"
|
||||
})
|
||||
const title = createMemo(() => provider()?.name ?? "Select model")
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
<DialogSelect<ReturnType<typeof options>[number]["value"]>
|
||||
options={options()}
|
||||
keybind={[
|
||||
{
|
||||
keybind: keybind.all.model_provider_list?.[0],
|
||||
@@ -223,12 +155,11 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
},
|
||||
},
|
||||
]}
|
||||
ref={setRef}
|
||||
onFilter={setQuery}
|
||||
flat={true}
|
||||
skipFilter={true}
|
||||
title={title()}
|
||||
current={local.model.current()}
|
||||
options={options()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,82 +26,67 @@ export function createDialogProviderOptions() {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
const connected = createMemo(() => new Set(sync.data.provider_next.connected))
|
||||
const options = createMemo(() => {
|
||||
return pipe(
|
||||
sync.data.provider_next.all,
|
||||
sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
|
||||
map((provider) => {
|
||||
const isConnected = connected().has(provider.id)
|
||||
return {
|
||||
title: provider.name,
|
||||
value: provider.id,
|
||||
description: {
|
||||
opencode: "(Recommended)",
|
||||
anthropic: "(Claude Max or API key)",
|
||||
openai: "(ChatGPT Plus/Pro or API key)",
|
||||
}[provider.id],
|
||||
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
||||
footer: isConnected ? "Connected" : undefined,
|
||||
async onSelect() {
|
||||
const methods = sync.data.provider_auth[provider.id] ?? [
|
||||
{
|
||||
type: "api",
|
||||
label: "API key",
|
||||
},
|
||||
]
|
||||
let index: number | null = 0
|
||||
if (methods.length > 1) {
|
||||
index = await new Promise<number | null>((resolve) => {
|
||||
dialog.replace(
|
||||
() => (
|
||||
<DialogSelect
|
||||
title="Select auth method"
|
||||
options={methods.map((x, index) => ({
|
||||
title: x.label,
|
||||
value: index,
|
||||
}))}
|
||||
onSelect={(option) => resolve(option.value)}
|
||||
/>
|
||||
),
|
||||
() => resolve(null),
|
||||
)
|
||||
})
|
||||
}
|
||||
if (index == null) return
|
||||
const method = methods[index]
|
||||
if (method.type === "oauth") {
|
||||
const result = await sdk.client.provider.oauth.authorize({
|
||||
providerID: provider.id,
|
||||
method: index,
|
||||
})
|
||||
if (result.data?.method === "code") {
|
||||
dialog.replace(() => (
|
||||
<CodeMethod
|
||||
providerID={provider.id}
|
||||
title={method.label}
|
||||
index={index}
|
||||
authorization={result.data!}
|
||||
map((provider) => ({
|
||||
title: provider.name,
|
||||
value: provider.id,
|
||||
description: {
|
||||
opencode: "(Recommended)",
|
||||
anthropic: "(Claude Max or API key)",
|
||||
openai: "(ChatGPT Plus/Pro or API key)",
|
||||
}[provider.id],
|
||||
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
||||
async onSelect() {
|
||||
const methods = sync.data.provider_auth[provider.id] ?? [
|
||||
{
|
||||
type: "api",
|
||||
label: "API key",
|
||||
},
|
||||
]
|
||||
let index: number | null = 0
|
||||
if (methods.length > 1) {
|
||||
index = await new Promise<number | null>((resolve) => {
|
||||
dialog.replace(
|
||||
() => (
|
||||
<DialogSelect
|
||||
title="Select auth method"
|
||||
options={methods.map((x, index) => ({
|
||||
title: x.label,
|
||||
value: index,
|
||||
}))}
|
||||
onSelect={(option) => resolve(option.value)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
if (result.data?.method === "auto") {
|
||||
dialog.replace(() => (
|
||||
<AutoMethod
|
||||
providerID={provider.id}
|
||||
title={method.label}
|
||||
index={index}
|
||||
authorization={result.data!}
|
||||
/>
|
||||
))
|
||||
}
|
||||
),
|
||||
() => resolve(null),
|
||||
)
|
||||
})
|
||||
}
|
||||
if (index == null) return
|
||||
const method = methods[index]
|
||||
if (method.type === "oauth") {
|
||||
const result = await sdk.client.provider.oauth.authorize({
|
||||
providerID: provider.id,
|
||||
method: index,
|
||||
})
|
||||
if (result.data?.method === "code") {
|
||||
dialog.replace(() => (
|
||||
<CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
||||
))
|
||||
}
|
||||
if (method.type === "api") {
|
||||
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
|
||||
if (result.data?.method === "auto") {
|
||||
dialog.replace(() => (
|
||||
<AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
||||
))
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
if (method.type === "api") {
|
||||
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
|
||||
}
|
||||
},
|
||||
})),
|
||||
)
|
||||
})
|
||||
return options
|
||||
@@ -124,7 +109,6 @@ function AutoMethod(props: AutoMethodProps) {
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
const toast = useToast()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.name === "c" && !evt.ctrl && !evt.meta) {
|
||||
@@ -155,16 +139,9 @@ function AutoMethod(props: AutoMethodProps) {
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
{props.title}
|
||||
</text>
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={hover() ? theme.primary : undefined}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => dialog.clear()}
|
||||
>
|
||||
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<box gap={1}>
|
||||
<Link href={props.authorization.url} fg={theme.primary} />
|
||||
|
||||
@@ -3,8 +3,7 @@ import { fileURLToPath } from "bun"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { For, Match, Switch, Show, createMemo, createSignal } from "solid-js"
|
||||
import { Installation } from "@/installation"
|
||||
import { For, Match, Switch, Show, createMemo } from "solid-js"
|
||||
|
||||
export type DialogStatusProps = {}
|
||||
|
||||
@@ -12,7 +11,6 @@ export function DialogStatus() {
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
const dialog = useDialog()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
|
||||
const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
|
||||
|
||||
@@ -47,18 +45,10 @@ export function DialogStatus() {
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
Status
|
||||
</text>
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={hover() ? theme.primary : undefined}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => dialog.clear()}
|
||||
>
|
||||
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted}>OpenCode v{Installation.VERSION}</text>
|
||||
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
|
||||
<box>
|
||||
<text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export type PromptInfo = {
|
||||
input: string
|
||||
mode?: "normal" | "shell" | "handoff"
|
||||
mode?: "normal" | "shell"
|
||||
parts: (
|
||||
| Omit<FilePart, "id" | "messageID" | "sessionID">
|
||||
| Omit<AgentPart, "id" | "messageID" | "sessionID">
|
||||
|
||||
@@ -119,7 +119,7 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
prompt: PromptInfo
|
||||
mode: "normal" | "shell" | "handoff"
|
||||
mode: "normal" | "shell"
|
||||
extmarkToPartIndex: Map<number, number>
|
||||
interrupt: number
|
||||
placeholder: number
|
||||
@@ -338,20 +338,6 @@ export function Prompt(props: PromptProps) {
|
||||
))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Handoff",
|
||||
value: "prompt.handoff",
|
||||
disabled: props.sessionID === undefined,
|
||||
category: "Prompt",
|
||||
slash: {
|
||||
name: "handoff",
|
||||
},
|
||||
onSelect: () => {
|
||||
input.clear()
|
||||
setStore("mode", "handoff")
|
||||
setStore("prompt", { input: "", parts: [] })
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -529,45 +515,17 @@ export function Prompt(props: PromptProps) {
|
||||
async function submit() {
|
||||
if (props.disabled) return
|
||||
if (autocomplete?.visible) return
|
||||
const selectedModel = local.model.current()
|
||||
if (!selectedModel) {
|
||||
promptModelWarning()
|
||||
return
|
||||
}
|
||||
|
||||
if (store.mode === "handoff") {
|
||||
const result = await sdk.client.session.handoff({
|
||||
sessionID: props.sessionID!,
|
||||
goal: store.prompt.input,
|
||||
model: {
|
||||
providerID: selectedModel.providerID,
|
||||
modelID: selectedModel.modelID,
|
||||
},
|
||||
})
|
||||
if (result.data) {
|
||||
route.navigate({
|
||||
type: "home",
|
||||
initialPrompt: {
|
||||
input: result.data.text,
|
||||
parts:
|
||||
result.data.files.map((file) => ({
|
||||
type: "file",
|
||||
url: file,
|
||||
filename: file,
|
||||
mime: "text/plain",
|
||||
})) ?? [],
|
||||
},
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!store.prompt.input) return
|
||||
const trimmed = store.prompt.input.trim()
|
||||
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
||||
exit()
|
||||
return
|
||||
}
|
||||
const selectedModel = local.model.current()
|
||||
if (!selectedModel) {
|
||||
promptModelWarning()
|
||||
return
|
||||
}
|
||||
const sessionID = props.sessionID
|
||||
? props.sessionID
|
||||
: await (async () => {
|
||||
@@ -768,7 +726,6 @@ export function Prompt(props: PromptProps) {
|
||||
const highlight = createMemo(() => {
|
||||
if (keybind.leader) return theme.border
|
||||
if (store.mode === "shell") return theme.primary
|
||||
if (store.mode === "handoff") return theme.warning
|
||||
return local.agent.color(local.agent.current().name)
|
||||
})
|
||||
|
||||
@@ -840,11 +797,7 @@ export function Prompt(props: PromptProps) {
|
||||
flexGrow={1}
|
||||
>
|
||||
<textarea
|
||||
placeholder={iife(() => {
|
||||
if (store.mode === "handoff") return "Goal for the new session"
|
||||
if (props.sessionID) return undefined
|
||||
return `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`
|
||||
})}
|
||||
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
||||
textColor={keybind.leader ? theme.textMuted : theme.text}
|
||||
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
||||
minHeight={1}
|
||||
@@ -901,7 +854,7 @@ export function Prompt(props: PromptProps) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (store.mode === "shell" || store.mode === "handoff") {
|
||||
if (store.mode === "shell") {
|
||||
if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
|
||||
setStore("mode", "normal")
|
||||
e.preventDefault()
|
||||
@@ -1022,11 +975,7 @@ export function Prompt(props: PromptProps) {
|
||||
/>
|
||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
|
||||
<text fg={highlight()}>
|
||||
<Switch>
|
||||
<Match when={store.mode === "normal"}>{Locale.titlecase(local.agent.current().name)}</Match>
|
||||
<Match when={store.mode === "shell"}>Shell</Match>
|
||||
<Match when={store.mode === "handoff"}>Handoff</Match>
|
||||
</Switch>
|
||||
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
|
||||
</text>
|
||||
<Show when={store.mode === "normal"}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
@@ -1173,11 +1122,6 @@ export function Prompt(props: PromptProps) {
|
||||
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={store.mode === "handoff"}>
|
||||
<text fg={theme.text}>
|
||||
esc <span style={{ fg: theme.textMuted }}>exit handoff mode</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
</Show>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user