mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-18 23:04:19 +00:00
Compare commits
79 Commits
migrate-co
...
config-spl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c9cae0d4f | ||
|
|
6fb4f2a7a5 | ||
|
|
48dfa45a9a | ||
|
|
97520c827e | ||
|
|
b75a89776d | ||
|
|
b909679367 | ||
|
|
639d1dd8fe | ||
|
|
7033b4d0a8 | ||
|
|
87c16374aa | ||
|
|
d366a1430f | ||
|
|
cfea5c73de | ||
|
|
2589eb207f | ||
|
|
ec7c72da3f | ||
|
|
a4b36a72ad | ||
|
|
e37a9081a6 | ||
|
|
a2469d933e | ||
|
|
3cde93bf2d | ||
|
|
898bcdec87 | ||
|
|
d5971e2da5 | ||
|
|
c71f4d4847 | ||
|
|
dec7827548 | ||
|
|
7faa8cb110 | ||
|
|
d8a4a125c0 | ||
|
|
50923f06f1 | ||
|
|
ba919fb619 | ||
|
|
47b4de3531 | ||
|
|
bb6d1d502f | ||
|
|
31e964e7cf | ||
|
|
06b2304a5f | ||
|
|
1b67339e4d | ||
|
|
1571246ba8 | ||
|
|
d730d8be01 | ||
|
|
e42cc85112 | ||
|
|
c7a79f1877 | ||
|
|
431f5347af | ||
|
|
1ed4a98233 | ||
|
|
db4ff89579 | ||
|
|
2f56761060 | ||
|
|
09286ccae0 | ||
|
|
4e959849f6 | ||
|
|
3690cafeb8 | ||
|
|
bcca253dec | ||
|
|
6d69ad5574 | ||
|
|
1f9be63e96 | ||
|
|
0873908030 | ||
|
|
4db2d94854 | ||
|
|
e5d52e4eb5 | ||
|
|
f20c0bffd3 | ||
|
|
9110e6a2a7 | ||
|
|
0888c02379 | ||
|
|
24ce49d9d7 | ||
|
|
5d69f00282 | ||
|
|
12016c8eb4 | ||
|
|
d6331cf792 | ||
|
|
2d7c9c9692 | ||
|
|
1aa18c6cd6 | ||
|
|
de25703e9d | ||
|
|
1133d87be0 | ||
|
|
42aa28d512 | ||
|
|
c6bd320003 | ||
|
|
24a9841322 | ||
|
|
8bf06cbcc1 | ||
|
|
be2e6f1926 | ||
|
|
72c12d59af | ||
|
|
8408e4702e | ||
|
|
ef14f64f9e | ||
|
|
3f60a6c2a4 | ||
|
|
d447b7694a | ||
|
|
5638b782c5 | ||
|
|
eb3f337695 | ||
|
|
c88ff3c08b | ||
|
|
e0e8b94384 | ||
|
|
8f4a72c57a | ||
|
|
ef155f3766 | ||
|
|
82a323ef70 | ||
|
|
a500eaa2d4 | ||
|
|
b714bb21d2 | ||
|
|
472d01fbaf | ||
|
|
a5c15a23e4 |
15
.github/TEAM_MEMBERS
vendored
Normal file
15
.github/TEAM_MEMBERS
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
adamdotdevin
|
||||
Brendonovich
|
||||
fwang
|
||||
Hona
|
||||
iamdavidhill
|
||||
jayair
|
||||
jlongster
|
||||
kitlangton
|
||||
kommander
|
||||
MrMushrooooom
|
||||
nexxeln
|
||||
R44VC0RP
|
||||
rekram1-node
|
||||
RhysSullivan
|
||||
thdxr
|
||||
11
.github/actions/setup-bun/action.yml
vendored
11
.github/actions/setup-bun/action.yml
vendored
@@ -3,12 +3,13 @@ description: "Setup Bun with caching and install dependencies"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Mount Bun Cache
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: useblacksmith/stickydisk@v1
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ github.repository }}-bun-cache-${{ runner.os }}
|
||||
path: ~/.bun
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
24
.github/pull_request_template.md
vendored
24
.github/pull_request_template.md
vendored
@@ -1,7 +1,29 @@
|
||||
### Issue for this PR
|
||||
|
||||
Closes #
|
||||
|
||||
### Type of change
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Refactor / code improvement
|
||||
- [ ] Documentation
|
||||
|
||||
### 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, 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!**
|
||||
|
||||
### How did you verify your code works?
|
||||
|
||||
### Screenshots / recordings
|
||||
|
||||
_If this is a UI change, please include a screenshot or recording._
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] I have tested my changes locally
|
||||
- [ ] I have not included unrelated changes in this PR
|
||||
|
||||
_If you do not follow this template your PR will be automatically rejected._
|
||||
|
||||
64
.github/workflows/duplicate-issues.yml
vendored
64
.github/workflows/duplicate-issues.yml
vendored
@@ -2,10 +2,11 @@ name: duplicate-issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -34,7 +35,7 @@ jobs:
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:
|
||||
opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created:
|
||||
|
||||
Issue number: ${{ github.event.issue.number }}
|
||||
|
||||
@@ -115,3 +116,62 @@ jobs:
|
||||
If you believe this was flagged incorrectly, please let a maintainer know.
|
||||
|
||||
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
|
||||
|
||||
recheck-compliance:
|
||||
if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance')
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Recheck compliance
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: |
|
||||
{
|
||||
"bash": {
|
||||
"*": "deny",
|
||||
"gh issue*": "allow"
|
||||
},
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited.
|
||||
|
||||
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
||||
|
||||
Re-check whether the issue now 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.
|
||||
|
||||
If the issue is NOW compliant:
|
||||
1. Remove the needs:compliance label: gh issue edit ${{ github.event.issue.number }} --remove-label needs:compliance
|
||||
2. Find and delete the previous compliance comment (the one containing <!-- issue-compliance -->) using: gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments --jq '.[] | select(.body | contains(\"<!-- issue-compliance -->\")) | .id' then delete it with: gh api -X DELETE repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments/{id}
|
||||
3. Post a short comment thanking them for updating the issue.
|
||||
|
||||
If the issue is STILL not compliant:
|
||||
Post a comment explaining what still needs to be fixed. Keep the needs:compliance label."
|
||||
|
||||
27
.github/workflows/pr-management.yml
vendored
27
.github/workflows/pr-management.yml
vendored
@@ -6,17 +6,6 @@ on:
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
if: |
|
||||
github.event.pull_request.user.login != 'actions-user' &&
|
||||
github.event.pull_request.user.login != 'opencode' &&
|
||||
github.event.pull_request.user.login != 'rekram1-node' &&
|
||||
github.event.pull_request.user.login != 'thdxr' &&
|
||||
github.event.pull_request.user.login != 'kommander' &&
|
||||
github.event.pull_request.user.login != 'jayair' &&
|
||||
github.event.pull_request.user.login != 'fwang' &&
|
||||
github.event.pull_request.user.login != 'adamdotdevin' &&
|
||||
github.event.pull_request.user.login != 'iamdavidhill' &&
|
||||
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -27,16 +16,31 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check team membership
|
||||
id: team-check
|
||||
run: |
|
||||
LOGIN="${{ github.event.pull_request.user.login }}"
|
||||
if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then
|
||||
echo "is_team=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Skipping: $LOGIN is a team member or bot"
|
||||
else
|
||||
echo "is_team=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
run: bun install
|
||||
|
||||
- name: Install opencode
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Build prompt
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
@@ -53,6 +57,7 @@ jobs:
|
||||
} > pr_info.txt
|
||||
|
||||
- name: Check for duplicate PRs
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
218
.github/workflows/pr-standards.yml
vendored
218
.github/workflows/pr-standards.yml
vendored
@@ -6,19 +6,9 @@ on:
|
||||
|
||||
jobs:
|
||||
check-standards:
|
||||
if: |
|
||||
github.event.pull_request.user.login != 'actions-user' &&
|
||||
github.event.pull_request.user.login != 'opencode' &&
|
||||
github.event.pull_request.user.login != 'rekram1-node' &&
|
||||
github.event.pull_request.user.login != 'thdxr' &&
|
||||
github.event.pull_request.user.login != 'kommander' &&
|
||||
github.event.pull_request.user.login != 'jayair' &&
|
||||
github.event.pull_request.user.login != 'fwang' &&
|
||||
github.event.pull_request.user.login != 'adamdotdevin' &&
|
||||
github.event.pull_request.user.login != 'iamdavidhill' &&
|
||||
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check PR standards
|
||||
@@ -26,6 +16,22 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const login = pr.user.login;
|
||||
|
||||
// Check if author is a team member or bot
|
||||
if (login === 'opencode-agent[bot]') return;
|
||||
const { data: file } = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: '.github/TEAM_MEMBERS',
|
||||
ref: 'dev'
|
||||
});
|
||||
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
|
||||
if (members.includes(login)) {
|
||||
console.log(`Skipping: ${login} is a team member`);
|
||||
return;
|
||||
}
|
||||
|
||||
const title = pr.title;
|
||||
|
||||
async function addLabel(label) {
|
||||
@@ -137,3 +143,193 @@ jobs:
|
||||
|
||||
await removeLabel('needs:issue');
|
||||
console.log('PR meets all standards');
|
||||
|
||||
check-compliance:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check PR template compliance
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const login = pr.user.login;
|
||||
|
||||
// Check if author is a team member or bot
|
||||
if (login === 'opencode-agent[bot]') return;
|
||||
const { data: file } = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: '.github/TEAM_MEMBERS',
|
||||
ref: 'dev'
|
||||
});
|
||||
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
|
||||
if (members.includes(login)) {
|
||||
console.log(`Skipping: ${login} is a team member`);
|
||||
return;
|
||||
}
|
||||
|
||||
const body = pr.body || '';
|
||||
const title = pr.title;
|
||||
const isDocsOrRefactor = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
|
||||
|
||||
const issues = [];
|
||||
|
||||
// Check: template sections exist
|
||||
const hasWhatSection = /### What does this PR do\?/.test(body);
|
||||
const hasTypeSection = /### Type of change/.test(body);
|
||||
const hasVerifySection = /### How did you verify your code works\?/.test(body);
|
||||
const hasChecklistSection = /### Checklist/.test(body);
|
||||
const hasIssueSection = /### Issue for this PR/.test(body);
|
||||
|
||||
if (!hasWhatSection || !hasTypeSection || !hasVerifySection || !hasChecklistSection || !hasIssueSection) {
|
||||
issues.push('PR description is missing required template sections. Please use the [PR template](../blob/dev/.github/pull_request_template.md).');
|
||||
}
|
||||
|
||||
// Check: "What does this PR do?" has real content (not just placeholder text)
|
||||
if (hasWhatSection) {
|
||||
const whatMatch = body.match(/### What does this PR do\?\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const whatContent = whatMatch ? whatMatch[1].trim() : '';
|
||||
const placeholder = 'Please provide a description of the issue';
|
||||
const onlyPlaceholder = whatContent.includes(placeholder) && whatContent.replace(placeholder, '').replace(/[*\s]/g, '').length < 20;
|
||||
if (!whatContent || onlyPlaceholder) {
|
||||
issues.push('"What does this PR do?" section is empty or only contains placeholder text. Please describe your changes.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check: at least one "Type of change" checkbox is checked
|
||||
if (hasTypeSection) {
|
||||
const typeMatch = body.match(/### Type of change\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const typeContent = typeMatch ? typeMatch[1] : '';
|
||||
const hasCheckedBox = /- \[x\]/i.test(typeContent);
|
||||
if (!hasCheckedBox) {
|
||||
issues.push('No "Type of change" checkbox is checked. Please select at least one.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check: issue reference (skip for docs/refactor)
|
||||
if (!isDocsOrRefactor && hasIssueSection) {
|
||||
const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const issueContent = issueMatch ? issueMatch[1].trim() : '';
|
||||
const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent);
|
||||
if (!hasIssueRef) {
|
||||
issues.push('No issue referenced. Please add `Closes #<number>` linking to the relevant issue.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check: "How did you verify" has content
|
||||
if (hasVerifySection) {
|
||||
const verifyMatch = body.match(/### How did you verify your code works\?\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const verifyContent = verifyMatch ? verifyMatch[1].trim() : '';
|
||||
if (!verifyContent) {
|
||||
issues.push('"How did you verify your code works?" section is empty. Please explain how you tested.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check: checklist boxes are checked
|
||||
if (hasChecklistSection) {
|
||||
const checklistMatch = body.match(/### Checklist\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const checklistContent = checklistMatch ? checklistMatch[1] : '';
|
||||
const unchecked = (checklistContent.match(/- \[ \]/g) || []).length;
|
||||
const checked = (checklistContent.match(/- \[x\]/gi) || []).length;
|
||||
if (checked < 2) {
|
||||
issues.push('Not all checklist items are checked. Please confirm you have tested locally and have not included unrelated changes.');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
async function addLabel(label) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: [label]
|
||||
});
|
||||
}
|
||||
|
||||
async function removeLabel(label) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
name: label
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const hasComplianceLabel = pr.labels.some(l => l.name === 'needs:compliance');
|
||||
|
||||
if (issues.length > 0) {
|
||||
// Non-compliant
|
||||
if (!hasComplianceLabel) {
|
||||
await addLabel('needs:compliance');
|
||||
}
|
||||
|
||||
const marker = '<!-- issue-compliance -->';
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
|
||||
const body_text = `${marker}
|
||||
This PR doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) and [PR template](../blob/dev/.github/pull_request_template.md).
|
||||
|
||||
**What needs to be fixed:**
|
||||
${issues.map(i => `- ${i}`).join('\n')}
|
||||
|
||||
Please edit this PR description to address the above within **2 hours**, or it will be automatically closed.
|
||||
|
||||
If you believe this was flagged incorrectly, please let a maintainer know.`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: body_text
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: body_text
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`PR #${pr.number} is non-compliant: ${issues.join(', ')}`);
|
||||
} else if (hasComplianceLabel) {
|
||||
// Was non-compliant, now fixed
|
||||
await removeLabel('needs:compliance');
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
const marker = '<!-- issue-compliance -->';
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id
|
||||
});
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: 'Thanks for updating your PR! It now meets our contributing guidelines. :+1:'
|
||||
});
|
||||
|
||||
console.log(`PR #${pr.number} is now compliant, label removed`);
|
||||
} else {
|
||||
console.log(`PR #${pr.number} is compliant`);
|
||||
}
|
||||
|
||||
108
bun.lock
108
bun.lock
@@ -18,7 +18,7 @@
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.17.23",
|
||||
"sst": "3.18.10",
|
||||
"turbo": "2.5.6",
|
||||
},
|
||||
},
|
||||
@@ -1612,7 +1612,7 @@
|
||||
|
||||
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="],
|
||||
|
||||
"@smithy/core": ["@smithy/core@3.23.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg=="],
|
||||
"@smithy/core": ["@smithy/core@3.23.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-HaaH4VbGie4t0+9nY3tNBRSxVTr96wzIqexUa6C2qx3MPePAuz7lIxPxYtt1Wc//SPfJLNoZJzfdt0B6ksj2jA=="],
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="],
|
||||
|
||||
@@ -1642,9 +1642,9 @@
|
||||
|
||||
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="],
|
||||
|
||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.14", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag=="],
|
||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.16", "", { "dependencies": { "@smithy/core": "^3.23.2", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-L5GICFCSsNhbJ5JSKeWFGFy16Q2OhoBizb3X2DrxaJwXSEujVvjG9Jt386dpQn2t7jINglQl0b4K/Su69BdbMA=="],
|
||||
|
||||
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.31", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg=="],
|
||||
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.33", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-jLqZOdJhtIL4lnA9hXnAG6GgnJlo1sD3FqsTxm9wSfjviqgWesY/TMBVnT84yr4O0Vfe0jWoXlfFbzsBVph3WA=="],
|
||||
|
||||
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="],
|
||||
|
||||
@@ -1668,7 +1668,7 @@
|
||||
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="],
|
||||
|
||||
"@smithy/smithy-client": ["@smithy/smithy-client@4.11.3", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg=="],
|
||||
"@smithy/smithy-client": ["@smithy/smithy-client@4.11.5", "", { "dependencies": { "@smithy/core": "^3.23.2", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-xixwBRqoeP2IUgcAl3U9dvJXc+qJum4lzo3maaJxifsZxKUYLfVfCXvhT4/jD01sRrHg5zjd1cw2Zmjr4/SuKQ=="],
|
||||
|
||||
"@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="],
|
||||
|
||||
@@ -1684,9 +1684,9 @@
|
||||
|
||||
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
|
||||
|
||||
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.30", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng=="],
|
||||
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.32", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-092sjYfFMQ/iaPH798LY/OJFBcYu0sSK34Oy9vdixhsU36zlZu8OcYjF3TD4e2ARupyK7xaxPXl+T0VIJTEkkg=="],
|
||||
|
||||
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.33", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA=="],
|
||||
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.35", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-miz/ggz87M8VuM29y7jJZMYkn7+IErM5p5UgKIf8OtqVs/h2bXr1Bt3uTsREsI/4nK8a0PQERbAPsVPVNIsG7Q=="],
|
||||
|
||||
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="],
|
||||
|
||||
@@ -1988,7 +1988,7 @@
|
||||
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="],
|
||||
|
||||
"@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="],
|
||||
"@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
|
||||
|
||||
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="],
|
||||
|
||||
@@ -2054,7 +2054,7 @@
|
||||
|
||||
"ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||
|
||||
@@ -2136,11 +2136,11 @@
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"b4a": ["b4a@1.7.4", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug=="],
|
||||
"b4a": ["b4a@1.7.5", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA=="],
|
||||
|
||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.3", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w=="],
|
||||
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.5", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg=="],
|
||||
|
||||
"babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
|
||||
|
||||
@@ -2216,13 +2216,13 @@
|
||||
|
||||
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
||||
|
||||
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eDgLN9teKTfmvrCqgwwmWNsNszxYs7IZdCqk0S1DCarvMhr4wcajoSBlA/nQA0/owwLduPTS8xxCnQp4/N/gDg=="],
|
||||
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qM7W5IaFpWYGPDcNiQ8DOng3noQ97gxpH2MFH1mGsdKwI0T4oy++egSh5Z7s6AQx8WKgc9GzAsTUM4KZkFdacw=="],
|
||||
|
||||
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-X+PjwJUWenUmdQBP8EtdItMyieQ6Nlpn+BH518oaouDiSnWj5+b0Y7DNDZJq7Ezom4EaxmqL/uGYZK3aCQ7CXg=="],
|
||||
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-oVoIsme27pcXB68YxnQSAgdNGCa4A3PGWYIBUewOh9VnJaoik4JenGb5Yy+svGE+ETFhQXV9nhHqgMPsDRrO6A=="],
|
||||
|
||||
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zMLs2YIGB+/jxrYFXaFhVKX/GBt05UTF45lc9srcHc9JXGjEj+12CIo1CHLTAWatXMTqt0Jsu6ukWEoWVT/ayA=="],
|
||||
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+SYt09k+xDEl/GfcU7L1zdNgm7IlvAFKV5Xl/auBwuprKG5UwXNhjRlRAWfhTMCUZWN+NDf8E+ZQx0cQi9K2/g=="],
|
||||
|
||||
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Z5yAK28xrcm8Wb5k7TZ8FJKpOI/r+aVCRdlHYAqI2SDJFN3nD4mJs900X6kNVmG/xFzb5yOuKVYWGg+6ZXWbyA=="],
|
||||
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-zvnUl4EAsQbKsmZVu+lEJcH8axQ7MiCfqg2OmnHd6uw1THABmHaX0GbpKiHshdgadNN2Nf+4zDyTJB5YMcAdrA=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
@@ -2242,7 +2242,7 @@
|
||||
|
||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
@@ -2476,7 +2476,7 @@
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="],
|
||||
|
||||
@@ -2712,7 +2712,7 @@
|
||||
|
||||
"h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="],
|
||||
|
||||
"happy-dom": ["happy-dom@20.6.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^6.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-+0vhESXXhFwkdjZnJ5DlmJIfUYGgIEEjzIjB+aKJbFuqlvvKyOi+XkI1fYbgYR9QCxG5T08koxsQ6HrQfa5gCQ=="],
|
||||
"happy-dom": ["happy-dom@20.6.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Xk/Y0cuq9ngN/my8uvK4gKoyDl6sBKkIl8A/hJ0IabZVH7E5SJLHNE7uKRPVmSrQbhJaLIHTEcvTct4GgNtsRA=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
@@ -2924,7 +2924,7 @@
|
||||
|
||||
"is-whitespace": ["is-whitespace@0.3.0", "", {}, "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||
|
||||
"is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="],
|
||||
|
||||
@@ -3502,7 +3502,7 @@
|
||||
|
||||
"punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="],
|
||||
|
||||
"qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
|
||||
@@ -3764,23 +3764,23 @@
|
||||
|
||||
"srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="],
|
||||
|
||||
"sst": ["sst@3.17.23", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.23", "sst-darwin-x64": "3.17.23", "sst-linux-arm64": "3.17.23", "sst-linux-x64": "3.17.23", "sst-linux-x86": "3.17.23", "sst-win32-arm64": "3.17.23", "sst-win32-x64": "3.17.23", "sst-win32-x86": "3.17.23" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-TwKgUgDnZdc1Swe+bvCNeyO4dQnYz5cTodMpYj3jlXZdK9/KNz0PVxT1f0u5E76i1pmilXrUBL/f7iiMPw4RDg=="],
|
||||
"sst": ["sst@3.18.10", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.18.10", "sst-darwin-x64": "3.18.10", "sst-linux-arm64": "3.18.10", "sst-linux-x64": "3.18.10", "sst-linux-x86": "3.18.10", "sst-win32-arm64": "3.18.10", "sst-win32-x64": "3.18.10", "sst-win32-x86": "3.18.10" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-SY+ldeJ9K5E9q+DhjXA3e2W3BEOzBwkE3IyLSD71uA3/5nRhUAST31iOWEpW36LbIvSQ9uOVDFcebztoLJ8s7w=="],
|
||||
|
||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R6kvmF+rUideOoU7KBs2SdvrIupoE+b+Dor/eq9Uo4Dojj7KvYDZI/EDm8sSCbbcx/opiWeyNqKtlnLEdCxE6g=="],
|
||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.18.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3MwIpMZhhdZKDqLp9ZQNlwkWix5+q+N0PWstuTomYwgZOxCCe6u9IIsoIszSk+GAJJN/jvGZyLiXKeV4iiQvw=="],
|
||||
|
||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-WW4P1S35iYCifQXxD+sE3wuzcN+LHLpuKMaNoaBqEcWGZnH3IPaDJ7rpLF0arkDAo/z3jZmWWzOCkr0JuqJ8vQ=="],
|
||||
"sst-darwin-x64": ["sst-darwin-x64@3.18.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-nQ0jMKkPOa+kj6Ygz8+kYhBua/vgNTLkd+4r8NSmk7v+Zs78lKnx3T//kEzS0yik6Q6QwGfokwrTcA1Jii2xSw=="],
|
||||
|
||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-TjtNqgIh7RlAWgPLFCAt0mXvIB+J7WjmRvIRrAdX0mXsndOiBJ/DMOgXSLVsIWHCfPj8MIEot/hWpnJgXgIeag=="],
|
||||
"sst-linux-arm64": ["sst-linux-arm64@3.18.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-mj9VNj3SvLS+HaXx2PhCX0aTA7CwJNoM6JhRc0s/zCilqchcvqDjbhpYBJO4brEPv6aOaaa7T3WvIQqtYauK4Q=="],
|
||||
|
||||
"sst-linux-x64": ["sst-linux-x64@3.17.23", "", { "os": "linux", "cpu": "x64" }, "sha512-qdqJiEbYfCjZlI3F/TA6eoIU7JXVkEEI/UMILNf2JWhky0KQdCW2Xyz+wb6c0msVJCWdUM/uj+1DaiP2eXvghw=="],
|
||||
"sst-linux-x64": ["sst-linux-x64@3.18.10", "", { "os": "linux", "cpu": "x64" }, "sha512-7iy1Eq2eqnT9Ag/8OVgC04vRjV7AAQyf/BvzLc+6Sz+GvRiKA8VEuPnbXNYQF+NIvEqsawfcd7MknSTtImpsvQ=="],
|
||||
|
||||
"sst-linux-x86": ["sst-linux-x86@3.17.23", "", { "os": "linux", "cpu": "none" }, "sha512-aGmUujIvoNlmAABEGsOgfY1rxD9koC6hN8bnTLbDI+oI/u/zjHYh50jsbL0p3TlaHpwF/lxP3xFSuT6IKp+KgA=="],
|
||||
"sst-linux-x86": ["sst-linux-x86@3.18.10", "", { "os": "linux", "cpu": "none" }, "sha512-77qZSuPZeQ5bdRCiq1pQEdY8EcGNHboKrx4P2yFid2FBDKJsXxOXtIxJdloyx+ljBn0+nxl/g040QBmXxdc9tA=="],
|
||||
|
||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZxdkGqYDrrZGz98rijDCN+m5yuCcwD6Bc9/6hubLsvdpNlVorUqzpg801Ec97xSK0nIC9g6pNiRyxAcsQQstUg=="],
|
||||
"sst-win32-arm64": ["sst-win32-arm64@3.18.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aY+FhMxvYs8crlrKALpLn/kKmud8YQj6LkMHsrOAAIJhfNyxhCja2vrYQaY+bcqdsS5W2LMVcS2hyaMqKXZKcg=="],
|
||||
|
||||
"sst-win32-x64": ["sst-win32-x64@3.17.23", "", { "os": "win32", "cpu": "x64" }, "sha512-yc9cor4MS49Ccy2tQCF1tf6M81yLeSGzGL+gjhUxpVKo2pN3bxl3w70eyU/mTXSEeyAmG9zEfbt6FNu4sy5cUA=="],
|
||||
"sst-win32-x64": ["sst-win32-x64@3.18.10", "", { "os": "win32", "cpu": "x64" }, "sha512-rY+yJXOpG+P5xXnaQRpCvBK2zwwLhjzpYidGkp6F+cGgiVdh2Wre/CIQNRaVHr20ncj8lLe/RsHWa9QCNM48jg=="],
|
||||
|
||||
"sst-win32-x86": ["sst-win32-x86@3.17.23", "", { "os": "win32", "cpu": "none" }, "sha512-DIp3s54IpNAfdYjSRt6McvkbEPQDMxUu6RUeRAd2C+FcTJgTloon/ghAPQBaDgu2VoVgymjcJARO/XyfKcCLOQ=="],
|
||||
"sst-win32-x86": ["sst-win32-x86@3.18.10", "", { "os": "win32", "cpu": "none" }, "sha512-pq8SmV0pIjBFMY6DraUZ4akyTxHnfjIKCRbBLdMxFUZK8TzA1NK2YdjRt1AwrgXRYGRyctrz/mt4WyO0SMOVQQ=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
@@ -3846,19 +3846,19 @@
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
|
||||
"tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="],
|
||||
"tar": ["tar@7.5.9", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
|
||||
"tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
|
||||
|
||||
"tedious": ["tedious@19.2.1", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.5", "@types/node": ">=18", "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA=="],
|
||||
"tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
|
||||
|
||||
"terracotta": ["terracotta@1.1.0", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww=="],
|
||||
|
||||
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
||||
|
||||
"text-decoder": ["text-decoder@1.2.4", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-mzlffA3tBNhziEHPK5L5InZg1d/ElNIpJhnhbDRNUtem/edZcJ5zg5FgwKKKOyklxk+6Jt+TrSu83musmvrDlg=="],
|
||||
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
@@ -3966,7 +3966,7 @@
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="],
|
||||
"undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="],
|
||||
|
||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
@@ -4132,7 +4132,7 @@
|
||||
|
||||
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
|
||||
|
||||
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
||||
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
@@ -4282,8 +4282,6 @@
|
||||
|
||||
"@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"@azure/core-http/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
||||
|
||||
"@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
||||
|
||||
"@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
@@ -4304,7 +4302,7 @@
|
||||
|
||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.21.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-26dQFi76dB8IiN/WKGQOV+yKKTTlRCxQjoi2WLt0kMcH8pvxVyvfdBDkld5GTl7W1qvBpwVOtFcsqktj3fBRpA=="],
|
||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.22.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
@@ -4506,7 +4504,9 @@
|
||||
|
||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="],
|
||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="],
|
||||
|
||||
@@ -4514,6 +4514,8 @@
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
@@ -4534,6 +4536,8 @@
|
||||
|
||||
"aws-sdk/events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="],
|
||||
|
||||
"aws-sdk/xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||
@@ -4544,6 +4548,10 @@
|
||||
|
||||
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"body-parser/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
|
||||
|
||||
"buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
|
||||
|
||||
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
@@ -4578,6 +4586,8 @@
|
||||
|
||||
"express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
||||
|
||||
"express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
|
||||
|
||||
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
@@ -4586,7 +4596,7 @@
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="],
|
||||
"glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="],
|
||||
|
||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
@@ -4624,8 +4634,6 @@
|
||||
|
||||
"mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
|
||||
|
||||
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
@@ -4654,10 +4662,10 @@
|
||||
|
||||
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
"parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
|
||||
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||
@@ -4714,6 +4722,8 @@
|
||||
|
||||
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@@ -4730,6 +4740,8 @@
|
||||
|
||||
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
|
||||
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
@@ -4750,6 +4762,8 @@
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
|
||||
|
||||
"yaml-language-server/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
|
||||
@@ -5010,8 +5024,6 @@
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="],
|
||||
@@ -5020,7 +5032,7 @@
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
"ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
@@ -5040,6 +5052,8 @@
|
||||
|
||||
"astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"aws-sdk/xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||
@@ -5074,8 +5088,6 @@
|
||||
|
||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
1
github/sst-env.d.ts
vendored
1
github/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../sst-env.d.ts" />
|
||||
|
||||
|
||||
@@ -214,9 +214,9 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
},
|
||||
transform: {
|
||||
server: {
|
||||
placement: { region: "aws:us-east-1" },
|
||||
transform: {
|
||||
worker: {
|
||||
placement: { mode: "smart" },
|
||||
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-5sXHoHbRdXbqM/zRJZiXt26sm/yyyZN/4OOHUtdofhk=",
|
||||
"aarch64-linux": "sha256-JCMm5X7e27BBV4wyaknCMM4CBt4Lr72SSvaGxEeNsJE=",
|
||||
"aarch64-darwin": "sha256-DBQJURlTPqFt0OYUHSvZZ4H0NUf020aic4zNX5CXzDc=",
|
||||
"x86_64-darwin": "sha256-t2luVxqCcRSgq/WNWkm4ZpKXO22n2RnAWP6msoTOr+A="
|
||||
"x86_64-linux": "sha256-7y6gQyIxyrdp2DaG/0oOEpuL+1n9oa8arUn1CuDiDhA=",
|
||||
"aarch64-linux": "sha256-7dnHO2WqQZ9A8cG3EC8p7408YR9n2F5C6DG5rNWHqNY=",
|
||||
"aarch64-darwin": "sha256-jxjhnVfE61RVOHaWvDO4mGLk6guQ8jHeXv/pbu5nbaE=",
|
||||
"x86_64-darwin": "sha256-22yM4FEtVxGWRug6H0rKog86Q/cYE3QsADrRbLeJKVQ="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.17.23",
|
||||
"sst": "3.18.10",
|
||||
"turbo": "2.5.6"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -403,15 +403,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const [composing, setComposing] = createSignal(false)
|
||||
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
|
||||
|
||||
createEffect(() => {
|
||||
if (!isFocused()) closePopover()
|
||||
})
|
||||
|
||||
// Safety: reset composing state on focus change to prevent stuck state
|
||||
// This handles edge cases where compositionend event may not fire
|
||||
createEffect(() => {
|
||||
if (!isFocused()) setComposing(false)
|
||||
})
|
||||
const handleBlur = () => {
|
||||
closePopover()
|
||||
setComposing(false)
|
||||
}
|
||||
|
||||
const agentList = createMemo(() =>
|
||||
sync.data.agent
|
||||
@@ -1118,6 +1113,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onPaste={handlePaste}
|
||||
onCompositionStart={() => setComposing(true)}
|
||||
onCompositionEnd={() => setComposing(false)}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
classList={{
|
||||
"select-text": true,
|
||||
@@ -1367,7 +1363,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t(mode === "shell" ? "command.prompt.mode.shell" : "command.prompt.mode.normal")}
|
||||
openDelay={2000}
|
||||
title={language.t(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
|
||||
keybind={command.keybind(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
|
||||
class="size-full flex items-center justify-center"
|
||||
>
|
||||
|
||||
@@ -257,27 +257,12 @@ export function SessionHeader() {
|
||||
] as const
|
||||
})
|
||||
|
||||
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 [menu, setMenu] = createStore({ open: false })
|
||||
|
||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
||||
|
||||
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")
|
||||
})
|
||||
|
||||
const openDir = (app: OpenApp) => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return
|
||||
@@ -319,9 +304,11 @@ export function SessionHeader() {
|
||||
<Show when={centerMount()}>
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
class="hidden md:flex w-[240px] max-w-full min-w-0 h-[24px] pl-0.5 pr-2 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
class="hidden md:flex w-[240px] max-w-full min-w-0 pl-0.5 pr-2 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel shadow-none cursor-default"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
aria-label={language.t("session.header.searchFiles")}
|
||||
>
|
||||
@@ -337,7 +324,7 @@ export function SessionHeader() {
|
||||
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind()}</Keybind>
|
||||
)}
|
||||
</Show>
|
||||
</button>
|
||||
</Button>
|
||||
</Portal>
|
||||
)}
|
||||
</Show>
|
||||
@@ -398,7 +385,7 @@ export function SessionHeader() {
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={prefs.app}
|
||||
value={current().id}
|
||||
onChange={(value) => {
|
||||
if (!OPEN_APPS.includes(value as OpenApp)) return
|
||||
setPrefs("app", value as OpenApp)
|
||||
@@ -464,7 +451,7 @@ export function SessionHeader() {
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
class:
|
||||
"rounded-md h-[24px] px-3 border border-border-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-active",
|
||||
"rounded-md h-[24px] px-3 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active",
|
||||
classList: { "rounded-r-none": share.shareUrl() !== undefined },
|
||||
style: { scale: 1 },
|
||||
}}
|
||||
@@ -537,7 +524,7 @@ export function SessionHeader() {
|
||||
<IconButton
|
||||
icon={share.state.copied ? "check" : "link"}
|
||||
variant="ghost"
|
||||
class="rounded-l-none h-[24px] border border-border-base bg-surface-panel shadow-none"
|
||||
class="rounded-l-none h-[24px] border border-border-weak-base bg-surface-panel shadow-none"
|
||||
onClick={() => share.copyLink((error) => showRequestError(language, error))}
|
||||
disabled={share.state.unshare}
|
||||
aria-label={
|
||||
|
||||
@@ -203,7 +203,7 @@ export function StatusPopover() {
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
class:
|
||||
"rounded-md h-[24px] pr-3 pl-0.5 gap-2 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-hover",
|
||||
"rounded-md h-[24px] pr-3 pl-0.5 gap-2 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active",
|
||||
style: { scale: 1 },
|
||||
}}
|
||||
trigger={
|
||||
|
||||
@@ -30,7 +30,6 @@ describe("pickDirectoriesToEvict", () => {
|
||||
describe("loadRootSessionsWithFallback", () => {
|
||||
test("uses limited roots query when supported", async () => {
|
||||
const calls: Array<{ directory: string; roots: true; limit?: number }> = []
|
||||
let fallback = 0
|
||||
|
||||
const result = await loadRootSessionsWithFallback({
|
||||
directory: "dir",
|
||||
@@ -39,20 +38,15 @@ describe("loadRootSessionsWithFallback", () => {
|
||||
calls.push(query)
|
||||
return { data: [] }
|
||||
},
|
||||
onFallback: () => {
|
||||
fallback += 1
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.data).toEqual([])
|
||||
expect(result.limited).toBe(true)
|
||||
expect(calls).toEqual([{ directory: "dir", roots: true, limit: 10 }])
|
||||
expect(fallback).toBe(0)
|
||||
})
|
||||
|
||||
test("falls back to full roots query on limited-query failure", async () => {
|
||||
const calls: Array<{ directory: string; roots: true; limit?: number }> = []
|
||||
let fallback = 0
|
||||
|
||||
const result = await loadRootSessionsWithFallback({
|
||||
directory: "dir",
|
||||
@@ -62,9 +56,6 @@ describe("loadRootSessionsWithFallback", () => {
|
||||
if (query.limit) throw new Error("unsupported")
|
||||
return { data: [] }
|
||||
},
|
||||
onFallback: () => {
|
||||
fallback += 1
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.data).toEqual([])
|
||||
@@ -73,7 +64,6 @@ describe("loadRootSessionsWithFallback", () => {
|
||||
{ directory: "dir", roots: true, limit: 25 },
|
||||
{ directory: "dir", roots: true },
|
||||
])
|
||||
expect(fallback).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -57,14 +57,6 @@ function errorMessage(error: unknown) {
|
||||
return "Unknown error"
|
||||
}
|
||||
|
||||
function setDevStats(value: {
|
||||
activeDirectoryStores: number
|
||||
evictions: number
|
||||
loadSessionsFullFetchFallback: number
|
||||
}) {
|
||||
;(globalThis as { __OPENCODE_GLOBAL_SYNC_STATS?: typeof value }).__OPENCODE_GLOBAL_SYNC_STATS = value
|
||||
}
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
@@ -72,11 +64,6 @@ function createGlobalSync() {
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||
|
||||
const stats = {
|
||||
evictions: 0,
|
||||
loadSessionsFallback: 0,
|
||||
}
|
||||
|
||||
const sdkCache = new Map<string, OpencodeClient>()
|
||||
const booting = new Map<string, Promise<void>>()
|
||||
const sessionLoads = new Map<string, Promise<void>>()
|
||||
@@ -112,15 +99,6 @@ function createGlobalSync() {
|
||||
setGlobalStore("session_todo", sessionID, reconcile(todos, { key: "id" }))
|
||||
}
|
||||
|
||||
const updateStats = (activeDirectoryStores: number) => {
|
||||
if (!import.meta.env.DEV) return
|
||||
setDevStats({
|
||||
activeDirectoryStores,
|
||||
evictions: stats.evictions,
|
||||
loadSessionsFullFetchFallback: stats.loadSessionsFallback,
|
||||
})
|
||||
}
|
||||
|
||||
const paused = () => untrack(() => globalStore.reload) !== undefined
|
||||
|
||||
const queue = createRefreshQueue({
|
||||
@@ -131,11 +109,6 @@ function createGlobalSync() {
|
||||
|
||||
const children = createChildStoreManager({
|
||||
owner,
|
||||
markStats: updateStats,
|
||||
incrementEvictions: () => {
|
||||
stats.evictions += 1
|
||||
updateStats(Object.keys(children.children).length)
|
||||
},
|
||||
isBooting: (directory) => booting.has(directory),
|
||||
isLoadingSessions: (directory) => sessionLoads.has(directory),
|
||||
onBootstrap: (directory) => {
|
||||
@@ -207,10 +180,6 @@ function createGlobalSync() {
|
||||
directory,
|
||||
limit,
|
||||
list: (query) => globalSDK.client.session.list(query),
|
||||
onFallback: () => {
|
||||
stats.loadSessionsFallback += 1
|
||||
updateStats(Object.keys(children.children).length)
|
||||
},
|
||||
})
|
||||
.then((x) => {
|
||||
const nonArchived = (x.data ?? [])
|
||||
|
||||
@@ -17,8 +17,6 @@ describe("createChildStoreManager", () => {
|
||||
|
||||
const manager = createChildStoreManager({
|
||||
owner,
|
||||
markStats() {},
|
||||
incrementEvictions() {},
|
||||
isBooting: () => false,
|
||||
isLoadingSessions: () => false,
|
||||
onBootstrap() {},
|
||||
|
||||
@@ -17,8 +17,6 @@ import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||
|
||||
export function createChildStoreManager(input: {
|
||||
owner: Owner
|
||||
markStats: (activeDirectoryStores: number) => void
|
||||
incrementEvictions: () => void
|
||||
isBooting: (directory: string) => boolean
|
||||
isLoadingSessions: (directory: string) => boolean
|
||||
onBootstrap: (directory: string) => void
|
||||
@@ -102,7 +100,6 @@ export function createChildStoreManager(input: {
|
||||
}
|
||||
delete children[directory]
|
||||
input.onDispose(directory)
|
||||
input.markStats(Object.keys(children).length)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -120,7 +117,6 @@ export function createChildStoreManager(input: {
|
||||
if (list.length === 0) return
|
||||
for (const directory of list) {
|
||||
if (!disposeDirectory(directory)) continue
|
||||
input.incrementEvictions()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +196,6 @@ export function createChildStoreManager(input: {
|
||||
})
|
||||
|
||||
runWithOwner(input.owner, init)
|
||||
input.markStats(Object.keys(children).length)
|
||||
}
|
||||
mark(directory)
|
||||
const childStore = children[directory]
|
||||
|
||||
@@ -9,7 +9,6 @@ export async function loadRootSessionsWithFallback(input: RootLoadArgs) {
|
||||
limited: true,
|
||||
} as const
|
||||
} catch {
|
||||
input.onFallback()
|
||||
const result = await input.list({ directory: input.directory, roots: true })
|
||||
return {
|
||||
data: result.data,
|
||||
|
||||
@@ -119,7 +119,6 @@ export type RootLoadArgs = {
|
||||
directory: string
|
||||
limit: number
|
||||
list: (query: { directory: string; roots: true; limit?: number }) => Promise<{ data?: Session[] }>
|
||||
onFallback: () => void
|
||||
}
|
||||
|
||||
export type RootLoadResult = {
|
||||
|
||||
@@ -174,6 +174,10 @@ function detectLocale(): Locale {
|
||||
return "en"
|
||||
}
|
||||
|
||||
function normalizeLocale(value: string): Locale {
|
||||
return LOCALES.includes(value as Locale) ? (value as Locale) : "en"
|
||||
}
|
||||
|
||||
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
||||
name: "Language",
|
||||
init: () => {
|
||||
@@ -184,15 +188,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
}),
|
||||
)
|
||||
|
||||
const locale = createMemo<Locale>(() =>
|
||||
LOCALES.includes(store.locale as Locale) ? (store.locale as Locale) : "en",
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const current = locale()
|
||||
if (store.locale === current) return
|
||||
setStore("locale", current)
|
||||
})
|
||||
const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
|
||||
|
||||
const dict = createMemo<Dictionary>(() => DICT[locale()])
|
||||
|
||||
@@ -213,7 +209,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
label,
|
||||
t,
|
||||
setLocale(next: Locale) {
|
||||
setStore("locale", next)
|
||||
setStore("locale", normalizeLocale(next))
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -63,8 +63,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق",
|
||||
"command.model.variant.cycle": "تغيير جهد التفكير",
|
||||
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
||||
"command.prompt.mode.shell": "التبديل إلى وضع Shell",
|
||||
"command.prompt.mode.normal": "التبديل إلى وضع Prompt",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
||||
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
||||
"command.workspace.toggle": "تبديل مساحات العمل",
|
||||
|
||||
@@ -63,8 +63,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Mudar para o agente anterior",
|
||||
"command.model.variant.cycle": "Alternar nível de raciocínio",
|
||||
"command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
|
||||
"command.prompt.mode.shell": "Alternar para o modo Shell",
|
||||
"command.prompt.mode.normal": "Alternar para o modo Prompt",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
|
||||
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
|
||||
"command.workspace.toggle": "Alternar espaços de trabalho",
|
||||
|
||||
@@ -69,8 +69,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta",
|
||||
"command.model.variant.cycle": "Promijeni nivo razmišljanja",
|
||||
"command.model.variant.cycle.description": "Prebaci na sljedeći nivo",
|
||||
"command.prompt.mode.shell": "Prebaci na Shell način",
|
||||
"command.prompt.mode.normal": "Prebaci na Prompt način",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
|
||||
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
|
||||
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
|
||||
|
||||
@@ -69,8 +69,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Skift til forrige agent",
|
||||
"command.model.variant.cycle": "Skift tænkeindsats",
|
||||
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
||||
"command.prompt.mode.shell": "Skift til shell-tilstand",
|
||||
"command.prompt.mode.normal": "Skift til prompt-tilstand",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
||||
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
||||
"command.workspace.toggle": "Skift arbejdsområder",
|
||||
|
||||
@@ -67,8 +67,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln",
|
||||
"command.model.variant.cycle": "Denkaufwand wechseln",
|
||||
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
||||
"command.prompt.mode.shell": "In den Shell-Modus wechseln",
|
||||
"command.prompt.mode.normal": "In den Prompt-Modus wechseln",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
||||
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
||||
"command.workspace.toggle": "Arbeitsbereiche umschalten",
|
||||
|
||||
@@ -69,8 +69,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
||||
"command.model.variant.cycle": "Cycle thinking effort",
|
||||
"command.model.variant.cycle.description": "Switch to the next effort level",
|
||||
"command.prompt.mode.shell": "Switch to shell mode",
|
||||
"command.prompt.mode.normal": "Switch to prompt mode",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
||||
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
|
||||
"command.workspace.toggle": "Toggle workspaces",
|
||||
|
||||
@@ -69,8 +69,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Cambiar al agente anterior",
|
||||
"command.model.variant.cycle": "Alternar esfuerzo de pensamiento",
|
||||
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
||||
"command.prompt.mode.shell": "Cambiar al modo Shell",
|
||||
"command.prompt.mode.normal": "Cambiar al modo Prompt",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
||||
"command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente",
|
||||
"command.workspace.toggle": "Alternar espacios de trabajo",
|
||||
|
||||
@@ -63,8 +63,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Passer à l'agent précédent",
|
||||
"command.model.variant.cycle": "Changer l'effort de réflexion",
|
||||
"command.model.variant.cycle.description": "Passer au niveau d'effort suivant",
|
||||
"command.prompt.mode.shell": "Passer en mode Shell",
|
||||
"command.prompt.mode.normal": "Passer en mode Prompt",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
||||
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
|
||||
"command.workspace.toggle": "Basculer les espaces de travail",
|
||||
|
||||
@@ -63,8 +63,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "前のエージェントに切り替え",
|
||||
"command.model.variant.cycle": "思考レベルの切り替え",
|
||||
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
||||
"command.prompt.mode.shell": "シェルモードに切り替える",
|
||||
"command.prompt.mode.normal": "プロンプトモードに切り替える",
|
||||
"command.prompt.mode.shell": "シェル",
|
||||
"command.prompt.mode.normal": "プロンプト",
|
||||
"command.permissions.autoaccept.enable": "編集を自動承認",
|
||||
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
||||
"command.workspace.toggle": "ワークスペースを切り替え",
|
||||
|
||||
@@ -67,8 +67,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "이전 에이전트로 전환",
|
||||
"command.model.variant.cycle": "생각 수준 순환",
|
||||
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
||||
"command.prompt.mode.shell": "셸 모드로 전환",
|
||||
"command.prompt.mode.normal": "프롬프트 모드로 전환",
|
||||
"command.prompt.mode.shell": "셸",
|
||||
"command.prompt.mode.normal": "프롬프트",
|
||||
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
||||
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
||||
"command.workspace.toggle": "작업 공간 전환",
|
||||
|
||||
@@ -72,8 +72,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Bytt til forrige agent",
|
||||
"command.model.variant.cycle": "Bytt tenkeinnsats",
|
||||
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
||||
"command.prompt.mode.shell": "Bytt til Shell-modus",
|
||||
"command.prompt.mode.normal": "Bytt til Prompt-modus",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
||||
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
||||
"command.workspace.toggle": "Veksle arbeidsområder",
|
||||
|
||||
@@ -63,8 +63,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta",
|
||||
"command.model.variant.cycle": "Przełącz wysiłek myślowy",
|
||||
"command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
|
||||
"command.prompt.mode.shell": "Przełącz na tryb terminala",
|
||||
"command.prompt.mode.normal": "Przełącz na tryb Prompt",
|
||||
"command.prompt.mode.shell": "Terminal",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
|
||||
"command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji",
|
||||
"command.workspace.toggle": "Przełącz przestrzenie robocze",
|
||||
|
||||
@@ -69,8 +69,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту",
|
||||
"command.model.variant.cycle": "Цикл режимов мышления",
|
||||
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
||||
"command.prompt.mode.shell": "Переключиться в режим оболочки",
|
||||
"command.prompt.mode.normal": "Переключиться в режим промпта",
|
||||
"command.prompt.mode.shell": "Оболочка",
|
||||
"command.prompt.mode.normal": "Промпт",
|
||||
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
||||
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
||||
"command.workspace.toggle": "Переключить рабочие пространства",
|
||||
|
||||
@@ -69,8 +69,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า",
|
||||
"command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด",
|
||||
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
||||
"command.prompt.mode.shell": "สลับไปยังโหมดเชลล์",
|
||||
"command.prompt.mode.normal": "สลับไปยังโหมดพรอมต์",
|
||||
"command.prompt.mode.shell": "เชลล์",
|
||||
"command.prompt.mode.normal": "พรอมต์",
|
||||
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"command.workspace.toggle": "สลับพื้นที่ทำงาน",
|
||||
|
||||
@@ -93,8 +93,8 @@ export const dict = {
|
||||
"command.model.variant.cycle": "切换思考强度",
|
||||
"command.model.variant.cycle.description": "切换到下一个强度等级",
|
||||
|
||||
"command.prompt.mode.shell": "切换到 Shell 模式",
|
||||
"command.prompt.mode.normal": "切换到 Prompt 模式",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
|
||||
"command.permissions.autoaccept.enable": "自动接受编辑",
|
||||
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
||||
|
||||
@@ -73,8 +73,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "切換到上一個代理程式",
|
||||
"command.model.variant.cycle": "循環思考強度",
|
||||
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
||||
"command.prompt.mode.shell": "切換到 Shell 模式",
|
||||
"command.prompt.mode.normal": "切換到 Prompt 模式",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "自動接受編輯",
|
||||
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
||||
"command.workspace.toggle": "切換工作區",
|
||||
|
||||
@@ -51,7 +51,6 @@ import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { DialogSettings } from "@/components/dialog-settings"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
||||
import { navStart } from "@/utils/perf"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogEditProject } from "@/components/dialog-edit-project"
|
||||
import { Titlebar } from "@/components/titlebar"
|
||||
@@ -178,7 +177,12 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
|
||||
const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering())
|
||||
const clearHoverProjectSoon = () => queueMicrotask(() => setState("hoverProject", undefined))
|
||||
const setHoverProject = (value: string | undefined) => {
|
||||
setState("hoverProject", value)
|
||||
if (value !== undefined) return
|
||||
aim.reset()
|
||||
}
|
||||
const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined))
|
||||
const setHoverSession = (id: string | undefined) => setState("hoverSession", id)
|
||||
|
||||
const hoverProjectData = createMemo(() => {
|
||||
@@ -189,13 +193,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
createEffect(() => {
|
||||
if (!layout.sidebar.opened()) return
|
||||
aim.reset()
|
||||
setState("hoverProject", undefined)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (state.hoverProject !== undefined) return
|
||||
aim.reset()
|
||||
setHoverProject(undefined)
|
||||
})
|
||||
|
||||
const autoselecting = createMemo(() => {
|
||||
@@ -226,7 +224,7 @@ export default function Layout(props: ParentProps) {
|
||||
const clearSidebarHoverState = () => {
|
||||
if (layout.sidebar.opened()) return
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
setHoverProject(undefined)
|
||||
}
|
||||
|
||||
const navigateWithSidebarReset = (href: string) => {
|
||||
@@ -826,14 +824,6 @@ export default function Layout(props: ParentProps) {
|
||||
if (next) prefetchSession(next)
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
navStart({
|
||||
dir: base64Encode(session.directory),
|
||||
from: params.id,
|
||||
to: session.id,
|
||||
trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup",
|
||||
})
|
||||
}
|
||||
navigateToSession(session)
|
||||
queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
|
||||
}
|
||||
@@ -869,15 +859,6 @@ export default function Layout(props: ParentProps) {
|
||||
if (next) prefetchSession(next)
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
navStart({
|
||||
dir: base64Encode(session.directory),
|
||||
from: params.id,
|
||||
to: session.id,
|
||||
trigger: offset > 0 ? "shift+alt+arrowdown" : "shift+alt+arrowup",
|
||||
})
|
||||
}
|
||||
|
||||
navigateToSession(session)
|
||||
queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
|
||||
return
|
||||
@@ -1508,7 +1489,7 @@ export default function Layout(props: ParentProps) {
|
||||
function handleDragStart(event: unknown) {
|
||||
const id = getDraggableId(event)
|
||||
if (!id) return
|
||||
setState("hoverProject", undefined)
|
||||
setHoverProject(undefined)
|
||||
setStore("activeProject", id)
|
||||
}
|
||||
|
||||
@@ -1942,7 +1923,7 @@ export default function Layout(props: ParentProps) {
|
||||
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
||||
navLeave.current = window.setTimeout(() => {
|
||||
navLeave.current = undefined
|
||||
setState("hoverProject", undefined)
|
||||
setHoverProject(undefined)
|
||||
setState("hoverSession", undefined)
|
||||
}, 300)
|
||||
}}
|
||||
|
||||
@@ -1,47 +1,29 @@
|
||||
import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
|
||||
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { checksum, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { findLast } from "@opencode-ai/util/array"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
import FileTree from "@/components/file-tree"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { SessionHeader, NewSessionView } from "@/components/session"
|
||||
import { navMark, navParams } from "@/utils/perf"
|
||||
import { same } from "@/utils/same"
|
||||
import { createOpenReviewFile } from "@/pages/session/helpers"
|
||||
import { createScrollSpy } from "@/pages/session/scroll-spy"
|
||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||
import {
|
||||
SessionReviewTab,
|
||||
StickyAddButton,
|
||||
type DiffStyle,
|
||||
type SessionReviewTabProps,
|
||||
} from "@/pages/session/review-tab"
|
||||
import { SessionReviewTab, type DiffStyle, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||
@@ -55,16 +37,13 @@ export default function Page() {
|
||||
const local = useLocal()
|
||||
const file = useFile()
|
||||
const sync = useSync()
|
||||
const terminal = useTerminal()
|
||||
const dialog = useDialog()
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
const prompt = usePrompt()
|
||||
const comments = useComments()
|
||||
const permission = usePermission()
|
||||
|
||||
const [ui, setUi] = createStore({
|
||||
pendingMessage: undefined as string | undefined,
|
||||
@@ -123,46 +102,6 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
createEffect(
|
||||
on(
|
||||
() => [params.dir, params.id] as const,
|
||||
([dir, id], prev) => {
|
||||
if (!id) return
|
||||
navParams({ dir, from: prev?.[1], to: id })
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
if (!prompt.ready()) return
|
||||
navMark({ dir: params.dir, to: id, name: "storage:prompt-ready" })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
if (!terminal.ready()) return
|
||||
navMark({ dir: params.dir, to: id, name: "storage:terminal-ready" })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
if (!file.ready()) return
|
||||
navMark({ dir: params.dir, to: id, name: "storage:file-view-ready" })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
if (sync.data.message[id] === undefined) return
|
||||
navMark({ dir: params.dir, to: id, name: "session:data-ready" })
|
||||
})
|
||||
}
|
||||
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
|
||||
@@ -195,16 +134,6 @@ export default function Page() {
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
}
|
||||
|
||||
const openTab = (value: string) => {
|
||||
const next = normalizeTab(value)
|
||||
tabs().open(next)
|
||||
|
||||
const path = file.pathFromTab(next)
|
||||
if (!path) return
|
||||
file.load(path)
|
||||
openReviewPanel()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const active = tabs().active()
|
||||
if (!active) return
|
||||
@@ -343,33 +272,6 @@ export default function Page() {
|
||||
scrollToMessage(msgs[targetIndex], "auto")
|
||||
}
|
||||
|
||||
const kinds = createMemo(() => {
|
||||
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
||||
if (!a) return b
|
||||
if (a === b) return a
|
||||
return "mix" as const
|
||||
}
|
||||
|
||||
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
||||
|
||||
const out = new Map<string, "add" | "del" | "mix">()
|
||||
for (const diff of diffs()) {
|
||||
const file = normalize(diff.file)
|
||||
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
||||
|
||||
out.set(file, kind)
|
||||
|
||||
const parts = file.split("/")
|
||||
for (const [idx] of parts.slice(0, -1).entries()) {
|
||||
const dir = parts.slice(0, idx + 1).join("/")
|
||||
if (!dir) continue
|
||||
out.set(dir, merge(out.get(dir), kind))
|
||||
}
|
||||
}
|
||||
return out
|
||||
})
|
||||
const emptyDiffFiles: string[] = []
|
||||
const diffFiles = createMemo(() => diffs().map((d) => d.file), emptyDiffFiles, { equals: same })
|
||||
const diffsReady = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return true
|
||||
@@ -377,7 +279,6 @@ export default function Page() {
|
||||
return sync.data.session_diff[id] !== undefined
|
||||
})
|
||||
|
||||
const idle = { type: "idle" as const }
|
||||
let inputRef!: HTMLDivElement
|
||||
let promptDock: HTMLDivElement | undefined
|
||||
let dockHeight = 0
|
||||
@@ -419,8 +320,6 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
sessionKey,
|
||||
@@ -453,11 +352,6 @@ export default function Page() {
|
||||
return lines.slice(0, 2).join("\n")
|
||||
}
|
||||
|
||||
const addSelectionToContext = (path: string, selection: FileSelection) => {
|
||||
const preview = selectionPreview(path, selection)
|
||||
prompt.context.add({ type: "file", path, selection, preview })
|
||||
}
|
||||
|
||||
const addCommentToContext = (input: {
|
||||
file: string
|
||||
selection: SelectedLineRange
|
||||
@@ -549,29 +443,8 @@ export default function Page() {
|
||||
const focusInput = () => inputRef?.focus()
|
||||
|
||||
useSessionCommands({
|
||||
command,
|
||||
dialog,
|
||||
file,
|
||||
language,
|
||||
local,
|
||||
permission,
|
||||
prompt,
|
||||
sdk,
|
||||
sync,
|
||||
terminal,
|
||||
layout,
|
||||
params,
|
||||
navigate,
|
||||
tabs,
|
||||
view,
|
||||
info,
|
||||
status,
|
||||
userMessages,
|
||||
visibleUserMessages,
|
||||
showAllFiles,
|
||||
navigateMessageByOffset,
|
||||
setActiveMessage,
|
||||
addSelectionToContext,
|
||||
focusInput,
|
||||
})
|
||||
|
||||
@@ -709,11 +582,6 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
const setFileTreeTabValue = (value: string) => {
|
||||
if (value !== "changes" && value !== "all") return
|
||||
setFileTreeTab(value)
|
||||
}
|
||||
|
||||
const reviewDiffId = (path: string) => {
|
||||
const sum = checksum(path)
|
||||
if (!sum) return
|
||||
@@ -809,12 +677,6 @@ export default function Page() {
|
||||
return "empty"
|
||||
})
|
||||
|
||||
const activeFileTab = createMemo(() => {
|
||||
const active = activeTab()
|
||||
if (!openedTabs().includes(active)) return
|
||||
return active
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!layout.ready()) return
|
||||
if (tabs().active()) return
|
||||
@@ -1119,7 +981,7 @@ export default function Page() {
|
||||
consumePendingMessage: layout.pendingMessage.consume,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
|
||||
@@ -1202,11 +1064,6 @@ export default function Page() {
|
||||
anchor={anchor}
|
||||
onRegisterMessage={scrollSpy.register}
|
||||
onUnregisterMessage={scrollSpy.unregister}
|
||||
onFirstTurnMount={() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
navMark({ dir: params.dir, to: id, name: "session:first-turn-mounted" })
|
||||
}}
|
||||
lastUserMessageID={lastUserMessage()?.id}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
@@ -168,6 +168,13 @@ export function FileTabContent(props: { tab: string }) {
|
||||
draftTop: undefined as number | undefined,
|
||||
})
|
||||
|
||||
const setCommenting = (range: SelectedLineRange | null) => {
|
||||
setNote("commenting", range)
|
||||
scheduleComments()
|
||||
if (!range) return
|
||||
setNote("draft", "")
|
||||
}
|
||||
|
||||
const getRoot = () => {
|
||||
const el = wrap
|
||||
if (!el) return
|
||||
@@ -260,13 +267,6 @@ export function FileTabContent(props: { tab: string }) {
|
||||
scheduleComments()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const range = note.commenting
|
||||
scheduleComments()
|
||||
if (!range) return
|
||||
setNote("draft", "")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const focus = comments.focus()
|
||||
const p = path()
|
||||
@@ -278,7 +278,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
if (!target) return
|
||||
|
||||
setNote("openedComment", target.id)
|
||||
setNote("commenting", null)
|
||||
setCommenting(null)
|
||||
file.setSelectedLines(p, target.selection)
|
||||
requestAnimationFrame(() => comments.clearFocus())
|
||||
})
|
||||
@@ -438,16 +438,16 @@ export function FileTabContent(props: { tab: string }) {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
file.setSelectedLines(p, range)
|
||||
if (!range) setNote("commenting", null)
|
||||
if (!range) setCommenting(null)
|
||||
}}
|
||||
onLineSelectionEnd={(range: SelectedLineRange | null) => {
|
||||
if (!range) {
|
||||
setNote("commenting", null)
|
||||
setCommenting(null)
|
||||
return
|
||||
}
|
||||
|
||||
setNote("openedComment", null)
|
||||
setNote("commenting", range)
|
||||
setCommenting(range)
|
||||
}}
|
||||
overflow="scroll"
|
||||
class="select-text"
|
||||
@@ -468,7 +468,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
onClick={() => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
setNote("commenting", null)
|
||||
setCommenting(null)
|
||||
setNote("openedComment", (current) => (current === comment.id ? null : comment.id))
|
||||
file.setSelectedLines(p, comment.selection)
|
||||
}}
|
||||
@@ -483,12 +483,12 @@ export function FileTabContent(props: { tab: string }) {
|
||||
value={note.draft}
|
||||
selection={formatCommentLabel(range())}
|
||||
onInput={(value) => setNote("draft", value)}
|
||||
onCancel={() => setNote("commenting", null)}
|
||||
onCancel={() => setCommenting(null)}
|
||||
onSubmit={(value) => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
addCommentToContext({ file: p, selection: range(), comment: value, origin: "file" })
|
||||
setNote("commenting", null)
|
||||
setCommenting(null)
|
||||
}}
|
||||
onPopoverFocusOut={(e: FocusEvent) => {
|
||||
const current = e.currentTarget as HTMLDivElement
|
||||
@@ -497,7 +497,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
|
||||
setTimeout(() => {
|
||||
if (!document.activeElement || !current.contains(document.activeElement)) {
|
||||
setNote("commenting", null)
|
||||
setCommenting(null)
|
||||
}
|
||||
}, 0)
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "./helpers"
|
||||
import { createOpenReviewFile, createOpenSessionFileTab, focusTerminalById, getTabReorderIndex } from "./helpers"
|
||||
|
||||
describe("createOpenReviewFile", () => {
|
||||
test("opens and loads selected review file", () => {
|
||||
@@ -20,6 +20,37 @@ describe("createOpenReviewFile", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("createOpenSessionFileTab", () => {
|
||||
test("activates the opened file tab", () => {
|
||||
const calls: string[] = []
|
||||
const openTab = createOpenSessionFileTab({
|
||||
normalizeTab: (value) => {
|
||||
calls.push(`normalize:${value}`)
|
||||
return `file://${value}`
|
||||
},
|
||||
openTab: (tab) => calls.push(`open:${tab}`),
|
||||
pathFromTab: (tab) => {
|
||||
calls.push(`path:${tab}`)
|
||||
return tab.slice("file://".length)
|
||||
},
|
||||
loadFile: (path) => calls.push(`load:${path}`),
|
||||
openReviewPanel: () => calls.push("review"),
|
||||
setActive: (tab) => calls.push(`active:${tab}`),
|
||||
})
|
||||
|
||||
openTab("src/a.ts")
|
||||
|
||||
expect(calls).toEqual([
|
||||
"normalize:src/a.ts",
|
||||
"open:file://src/a.ts",
|
||||
"path:file://src/a.ts",
|
||||
"load:src/a.ts",
|
||||
"review",
|
||||
"active:file://src/a.ts",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("focusTerminalById", () => {
|
||||
test("focuses textarea when present", () => {
|
||||
document.body.innerHTML = `<div id="terminal-wrapper-one"><div data-component="terminal"><textarea></textarea></div></div>`
|
||||
|
||||
@@ -35,6 +35,27 @@ export const createOpenReviewFile = (input: {
|
||||
}
|
||||
}
|
||||
|
||||
export const createOpenSessionFileTab = (input: {
|
||||
normalizeTab: (tab: string) => string
|
||||
openTab: (tab: string) => void
|
||||
pathFromTab: (tab: string) => string | undefined
|
||||
loadFile: (path: string) => void
|
||||
openReviewPanel: () => void
|
||||
setActive: (tab: string) => void
|
||||
}) => {
|
||||
return (value: string) => {
|
||||
const next = input.normalizeTab(value)
|
||||
input.openTab(next)
|
||||
|
||||
const path = input.pathFromTab(next)
|
||||
if (!path) return
|
||||
|
||||
input.loadFile(path)
|
||||
input.openReviewPanel()
|
||||
input.setActive(next)
|
||||
}
|
||||
}
|
||||
|
||||
export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => {
|
||||
const fromIndex = tabs.indexOf(from)
|
||||
const toIndex = tabs.indexOf(to)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, createEffect, createMemo, on, onCleanup, onMount, Show, type JSX } from "solid-js"
|
||||
import { For, createEffect, createMemo, on, onCleanup, Show, type JSX } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
@@ -72,7 +72,6 @@ export function MessageTimeline(props: {
|
||||
anchor: (id: string) => string
|
||||
onRegisterMessage: (el: HTMLDivElement, id: string) => void
|
||||
onUnregisterMessage: (id: string) => void
|
||||
onFirstTurnMount?: () => void
|
||||
lastUserMessageID?: string
|
||||
}) {
|
||||
let touchGesture: number | undefined
|
||||
@@ -516,37 +515,31 @@ export function MessageTimeline(props: {
|
||||
</div>
|
||||
</Show>
|
||||
<For each={props.renderedUserMessages}>
|
||||
{(message) => {
|
||||
if (import.meta.env.DEV && props.onFirstTurnMount) {
|
||||
onMount(() => props.onFirstTurnMount?.())
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={props.anchor(message.id)}
|
||||
data-message-id={message.id}
|
||||
ref={(el) => {
|
||||
props.onRegisterMessage(el, message.id)
|
||||
onCleanup(() => props.onUnregisterMessage(message.id))
|
||||
{(message) => (
|
||||
<div
|
||||
id={props.anchor(message.id)}
|
||||
data-message-id={message.id}
|
||||
ref={(el) => {
|
||||
props.onRegisterMessage(el, message.id)
|
||||
onCleanup(() => props.onUnregisterMessage(message.id))
|
||||
}}
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<SessionTurn
|
||||
sessionID={sessionID() ?? ""}
|
||||
messageID={message.id}
|
||||
lastUserMessageID={props.lastUserMessageID}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content: "flex flex-col justify-between !overflow-visible",
|
||||
container: "w-full px-4 md:px-6",
|
||||
}}
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<SessionTurn
|
||||
sessionID={sessionID() ?? ""}
|
||||
messageID={message.id}
|
||||
lastUserMessageID={props.lastUserMessageID}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content: "flex flex-col justify-between !overflow-visible",
|
||||
container: "w-full px-4 md:px-6",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,8 +144,8 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
onOpenChange={props.view().review.setOpen}
|
||||
classes={{
|
||||
root: props.classes?.root ?? "pb-6",
|
||||
header: props.classes?.header ?? "px-6",
|
||||
container: props.classes?.container ?? "px-6",
|
||||
header: props.classes?.header ?? "px-3",
|
||||
container: props.classes?.container ?? "px-3",
|
||||
}}
|
||||
diffs={props.diffs()}
|
||||
diffStyle={props.diffStyle}
|
||||
|
||||
@@ -70,29 +70,28 @@ export function SessionPromptDock(props: {
|
||||
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
||||
})
|
||||
|
||||
const [responding, setResponding] = createSignal(false)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => permissionRequest()?.id,
|
||||
() => setResponding(false),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
const [responding, setResponding] = createSignal<string | undefined>()
|
||||
const permissionResponding = () => {
|
||||
const perm = permissionRequest()
|
||||
if (!perm) return false
|
||||
return responding() === perm.id
|
||||
}
|
||||
|
||||
const decide = (response: "once" | "always" | "reject") => {
|
||||
const perm = permissionRequest()
|
||||
if (!perm) return
|
||||
if (responding()) return
|
||||
if (responding() === perm.id) return
|
||||
|
||||
setResponding(true)
|
||||
setResponding(perm.id)
|
||||
sdk.client.permission
|
||||
.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
|
||||
.catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
.finally(() => setResponding(false))
|
||||
.finally(() => {
|
||||
setResponding((id) => (id === perm.id ? undefined : id))
|
||||
})
|
||||
}
|
||||
|
||||
const done = createMemo(
|
||||
@@ -218,18 +217,28 @@ export function SessionPromptDock(props: {
|
||||
<>
|
||||
<div />
|
||||
<div data-slot="permission-footer-actions">
|
||||
<Button variant="ghost" size="normal" onClick={() => decide("reject")} disabled={responding()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
onClick={() => decide("reject")}
|
||||
disabled={permissionResponding()}
|
||||
>
|
||||
{language.t("ui.permission.deny")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="normal"
|
||||
onClick={() => decide("always")}
|
||||
disabled={responding()}
|
||||
disabled={permissionResponding()}
|
||||
>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Button variant="primary" size="normal" onClick={() => decide("once")} disabled={responding()}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="normal"
|
||||
onClick={() => decide("once")}
|
||||
disabled={permissionResponding()}
|
||||
>
|
||||
{language.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useLayout } from "@/context/layout"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||
import { getTabReorderIndex } from "@/pages/session/helpers"
|
||||
import { createOpenSessionFileTab, getTabReorderIndex } from "@/pages/session/helpers"
|
||||
import { StickyAddButton } from "@/pages/session/review-tab"
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
|
||||
@@ -96,15 +96,14 @@ export function SessionSidePanel(props: {
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
}
|
||||
|
||||
const openTab = (value: string) => {
|
||||
const next = normalizeTab(value)
|
||||
tabs().open(next)
|
||||
|
||||
const path = file.pathFromTab(next)
|
||||
if (!path) return
|
||||
file.load(path)
|
||||
openReviewPanel()
|
||||
}
|
||||
const openTab = createOpenSessionFileTab({
|
||||
normalizeTab,
|
||||
openTab: tabs().open,
|
||||
pathFromTab: file.pathFromTab,
|
||||
loadFile: file.load,
|
||||
openReviewPanel,
|
||||
setActive: tabs().setActive,
|
||||
})
|
||||
|
||||
const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
|
||||
const openedTabs = createMemo(() =>
|
||||
@@ -355,7 +354,7 @@ export function SessionSidePanel(props: {
|
||||
{language.t("session.files.all")}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="changes" class="bg-background-base px-3 py-0">
|
||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={hasReview()}>
|
||||
<Show
|
||||
@@ -384,7 +383,7 @@ export function SessionSidePanel(props: {
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="all" class="bg-background-base px-3 py-0">
|
||||
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
||||
<FileTree
|
||||
path=""
|
||||
modified={diffFiles()}
|
||||
|
||||
@@ -22,29 +22,8 @@ import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { canAddSelectionContext } from "@/pages/session/session-command-helpers"
|
||||
|
||||
export type SessionCommandContext = {
|
||||
command: ReturnType<typeof useCommand>
|
||||
dialog: ReturnType<typeof useDialog>
|
||||
file: ReturnType<typeof useFile>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
local: ReturnType<typeof useLocal>
|
||||
permission: ReturnType<typeof usePermission>
|
||||
prompt: ReturnType<typeof usePrompt>
|
||||
sdk: ReturnType<typeof useSDK>
|
||||
sync: ReturnType<typeof useSync>
|
||||
terminal: ReturnType<typeof useTerminal>
|
||||
layout: ReturnType<typeof useLayout>
|
||||
params: ReturnType<typeof useParams>
|
||||
navigate: ReturnType<typeof useNavigate>
|
||||
tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
||||
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
||||
info: () => { revert?: { messageID?: string }; share?: { url?: string } } | undefined
|
||||
status: () => { type: string }
|
||||
userMessages: () => UserMessage[]
|
||||
visibleUserMessages: () => UserMessage[]
|
||||
showAllFiles: () => void
|
||||
navigateMessageByOffset: (offset: number) => void
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
addSelectionToContext: (path: string, selection: FileSelection) => void
|
||||
focusInput: () => void
|
||||
}
|
||||
|
||||
@@ -55,45 +34,98 @@ const withCategory = (category: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
const sessionCommand = withCategory(input.language.t("command.category.session"))
|
||||
const fileCommand = withCategory(input.language.t("command.category.file"))
|
||||
const contextCommand = withCategory(input.language.t("command.category.context"))
|
||||
const viewCommand = withCategory(input.language.t("command.category.view"))
|
||||
const terminalCommand = withCategory(input.language.t("command.category.terminal"))
|
||||
const modelCommand = withCategory(input.language.t("command.category.model"))
|
||||
const mcpCommand = withCategory(input.language.t("command.category.mcp"))
|
||||
const agentCommand = withCategory(input.language.t("command.category.agent"))
|
||||
const permissionsCommand = withCategory(input.language.t("command.category.permissions"))
|
||||
export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const command = useCommand()
|
||||
const dialog = useDialog()
|
||||
const file = useFile()
|
||||
const language = useLanguage()
|
||||
const local = useLocal()
|
||||
const permission = usePermission()
|
||||
const prompt = usePrompt()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const terminal = useTerminal()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
|
||||
const idle = { type: "idle" as const }
|
||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[])
|
||||
const visibleUserMessages = createMemo(() => {
|
||||
const revert = info()?.revert?.messageID
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
})
|
||||
|
||||
const showAllFiles = () => {
|
||||
if (layout.fileTree.tab() !== "changes") return
|
||||
layout.fileTree.setTab("all")
|
||||
}
|
||||
|
||||
const selectionPreview = (path: string, selection: FileSelection) => {
|
||||
const content = file.get(path)?.content?.content
|
||||
if (!content) return undefined
|
||||
const start = Math.max(1, Math.min(selection.startLine, selection.endLine))
|
||||
const end = Math.max(selection.startLine, selection.endLine)
|
||||
const lines = content.split("\n").slice(start - 1, end)
|
||||
if (lines.length === 0) return undefined
|
||||
return lines.slice(0, 2).join("\n")
|
||||
}
|
||||
|
||||
const addSelectionToContext = (path: string, selection: FileSelection) => {
|
||||
const preview = selectionPreview(path, selection)
|
||||
prompt.context.add({ type: "file", path, selection, preview })
|
||||
}
|
||||
|
||||
const navigateMessageByOffset = actions.navigateMessageByOffset
|
||||
const setActiveMessage = actions.setActiveMessage
|
||||
const focusInput = actions.focusInput
|
||||
|
||||
const sessionCommand = withCategory(language.t("command.category.session"))
|
||||
const fileCommand = withCategory(language.t("command.category.file"))
|
||||
const contextCommand = withCategory(language.t("command.category.context"))
|
||||
const viewCommand = withCategory(language.t("command.category.view"))
|
||||
const terminalCommand = withCategory(language.t("command.category.terminal"))
|
||||
const modelCommand = withCategory(language.t("command.category.model"))
|
||||
const mcpCommand = withCategory(language.t("command.category.mcp"))
|
||||
const agentCommand = withCategory(language.t("command.category.agent"))
|
||||
const permissionsCommand = withCategory(language.t("command.category.permissions"))
|
||||
|
||||
const sessionCommands = createMemo(() => [
|
||||
sessionCommand({
|
||||
id: "session.new",
|
||||
title: input.language.t("command.session.new"),
|
||||
title: language.t("command.session.new"),
|
||||
keybind: "mod+shift+s",
|
||||
slash: "new",
|
||||
onSelect: () => input.navigate(`/${input.params.dir}/session`),
|
||||
onSelect: () => navigate(`/${params.dir}/session`),
|
||||
}),
|
||||
])
|
||||
|
||||
const fileCommands = createMemo(() => [
|
||||
fileCommand({
|
||||
id: "file.open",
|
||||
title: input.language.t("command.file.open"),
|
||||
description: input.language.t("palette.search.placeholder"),
|
||||
title: language.t("command.file.open"),
|
||||
description: language.t("palette.search.placeholder"),
|
||||
keybind: "mod+p",
|
||||
slash: "open",
|
||||
onSelect: () => input.dialog.show(() => <DialogSelectFile onOpenFile={input.showAllFiles} />),
|
||||
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
||||
}),
|
||||
fileCommand({
|
||||
id: "tab.close",
|
||||
title: input.language.t("command.tab.close"),
|
||||
title: language.t("command.tab.close"),
|
||||
keybind: "mod+w",
|
||||
disabled: !input.tabs().active(),
|
||||
disabled: !tabs().active(),
|
||||
onSelect: () => {
|
||||
const active = input.tabs().active()
|
||||
const active = tabs().active()
|
||||
if (!active) return
|
||||
input.tabs().close(active)
|
||||
tabs().close(active)
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -101,30 +133,30 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
const contextCommands = createMemo(() => [
|
||||
contextCommand({
|
||||
id: "context.addSelection",
|
||||
title: input.language.t("command.context.addSelection"),
|
||||
description: input.language.t("command.context.addSelection.description"),
|
||||
title: language.t("command.context.addSelection"),
|
||||
description: language.t("command.context.addSelection.description"),
|
||||
keybind: "mod+shift+l",
|
||||
disabled: !canAddSelectionContext({
|
||||
active: input.tabs().active(),
|
||||
pathFromTab: input.file.pathFromTab,
|
||||
selectedLines: input.file.selectedLines,
|
||||
active: tabs().active(),
|
||||
pathFromTab: file.pathFromTab,
|
||||
selectedLines: file.selectedLines,
|
||||
}),
|
||||
onSelect: () => {
|
||||
const active = input.tabs().active()
|
||||
const active = tabs().active()
|
||||
if (!active) return
|
||||
const path = input.file.pathFromTab(active)
|
||||
const path = file.pathFromTab(active)
|
||||
if (!path) return
|
||||
|
||||
const range = input.file.selectedLines(path) as SelectedLineRange | null | undefined
|
||||
const range = file.selectedLines(path) as SelectedLineRange | null | undefined
|
||||
if (!range) {
|
||||
showToast({
|
||||
title: input.language.t("toast.context.noLineSelection.title"),
|
||||
description: input.language.t("toast.context.noLineSelection.description"),
|
||||
title: language.t("toast.context.noLineSelection.title"),
|
||||
description: language.t("toast.context.noLineSelection.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
input.addSelectionToContext(path, selectionFromLines(range))
|
||||
addSelectionToContext(path, selectionFromLines(range))
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -132,37 +164,37 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
const viewCommands = createMemo(() => [
|
||||
viewCommand({
|
||||
id: "terminal.toggle",
|
||||
title: input.language.t("command.terminal.toggle"),
|
||||
title: language.t("command.terminal.toggle"),
|
||||
keybind: "ctrl+`",
|
||||
slash: "terminal",
|
||||
onSelect: () => input.view().terminal.toggle(),
|
||||
onSelect: () => view().terminal.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "review.toggle",
|
||||
title: input.language.t("command.review.toggle"),
|
||||
title: language.t("command.review.toggle"),
|
||||
keybind: "mod+shift+r",
|
||||
onSelect: () => input.view().reviewPanel.toggle(),
|
||||
onSelect: () => view().reviewPanel.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "fileTree.toggle",
|
||||
title: input.language.t("command.fileTree.toggle"),
|
||||
title: language.t("command.fileTree.toggle"),
|
||||
keybind: "mod+\\",
|
||||
onSelect: () => input.layout.fileTree.toggle(),
|
||||
onSelect: () => layout.fileTree.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "input.focus",
|
||||
title: input.language.t("command.input.focus"),
|
||||
title: language.t("command.input.focus"),
|
||||
keybind: "ctrl+l",
|
||||
onSelect: () => input.focusInput(),
|
||||
onSelect: () => focusInput(),
|
||||
}),
|
||||
terminalCommand({
|
||||
id: "terminal.new",
|
||||
title: input.language.t("command.terminal.new"),
|
||||
description: input.language.t("command.terminal.new.description"),
|
||||
title: language.t("command.terminal.new"),
|
||||
description: language.t("command.terminal.new.description"),
|
||||
keybind: "ctrl+alt+t",
|
||||
onSelect: () => {
|
||||
if (input.terminal.all().length > 0) input.terminal.new()
|
||||
input.view().terminal.open()
|
||||
if (terminal.all().length > 0) terminal.new()
|
||||
view().terminal.open()
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -170,61 +202,61 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
const messageCommands = createMemo(() => [
|
||||
sessionCommand({
|
||||
id: "message.previous",
|
||||
title: input.language.t("command.message.previous"),
|
||||
description: input.language.t("command.message.previous.description"),
|
||||
title: language.t("command.message.previous"),
|
||||
description: language.t("command.message.previous.description"),
|
||||
keybind: "mod+arrowup",
|
||||
disabled: !input.params.id,
|
||||
onSelect: () => input.navigateMessageByOffset(-1),
|
||||
disabled: !params.id,
|
||||
onSelect: () => navigateMessageByOffset(-1),
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "message.next",
|
||||
title: input.language.t("command.message.next"),
|
||||
description: input.language.t("command.message.next.description"),
|
||||
title: language.t("command.message.next"),
|
||||
description: language.t("command.message.next.description"),
|
||||
keybind: "mod+arrowdown",
|
||||
disabled: !input.params.id,
|
||||
onSelect: () => input.navigateMessageByOffset(1),
|
||||
disabled: !params.id,
|
||||
onSelect: () => navigateMessageByOffset(1),
|
||||
}),
|
||||
])
|
||||
|
||||
const agentCommands = createMemo(() => [
|
||||
modelCommand({
|
||||
id: "model.choose",
|
||||
title: input.language.t("command.model.choose"),
|
||||
description: input.language.t("command.model.choose.description"),
|
||||
title: language.t("command.model.choose"),
|
||||
description: language.t("command.model.choose.description"),
|
||||
keybind: "mod+'",
|
||||
slash: "model",
|
||||
onSelect: () => input.dialog.show(() => <DialogSelectModel />),
|
||||
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
||||
}),
|
||||
mcpCommand({
|
||||
id: "mcp.toggle",
|
||||
title: input.language.t("command.mcp.toggle"),
|
||||
description: input.language.t("command.mcp.toggle.description"),
|
||||
title: language.t("command.mcp.toggle"),
|
||||
description: language.t("command.mcp.toggle.description"),
|
||||
keybind: "mod+;",
|
||||
slash: "mcp",
|
||||
onSelect: () => input.dialog.show(() => <DialogSelectMcp />),
|
||||
onSelect: () => dialog.show(() => <DialogSelectMcp />),
|
||||
}),
|
||||
agentCommand({
|
||||
id: "agent.cycle",
|
||||
title: input.language.t("command.agent.cycle"),
|
||||
description: input.language.t("command.agent.cycle.description"),
|
||||
title: language.t("command.agent.cycle"),
|
||||
description: language.t("command.agent.cycle.description"),
|
||||
keybind: "mod+.",
|
||||
slash: "agent",
|
||||
onSelect: () => input.local.agent.move(1),
|
||||
onSelect: () => local.agent.move(1),
|
||||
}),
|
||||
agentCommand({
|
||||
id: "agent.cycle.reverse",
|
||||
title: input.language.t("command.agent.cycle.reverse"),
|
||||
description: input.language.t("command.agent.cycle.reverse.description"),
|
||||
title: language.t("command.agent.cycle.reverse"),
|
||||
description: language.t("command.agent.cycle.reverse.description"),
|
||||
keybind: "shift+mod+.",
|
||||
onSelect: () => input.local.agent.move(-1),
|
||||
onSelect: () => local.agent.move(-1),
|
||||
}),
|
||||
modelCommand({
|
||||
id: "model.variant.cycle",
|
||||
title: input.language.t("command.model.variant.cycle"),
|
||||
description: input.language.t("command.model.variant.cycle.description"),
|
||||
title: language.t("command.model.variant.cycle"),
|
||||
description: language.t("command.model.variant.cycle.description"),
|
||||
keybind: "shift+mod+d",
|
||||
onSelect: () => {
|
||||
input.local.model.variant.cycle()
|
||||
local.model.variant.cycle()
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -233,22 +265,22 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
permissionsCommand({
|
||||
id: "permissions.autoaccept",
|
||||
title:
|
||||
input.params.id && input.permission.isAutoAccepting(input.params.id, input.sdk.directory)
|
||||
? input.language.t("command.permissions.autoaccept.disable")
|
||||
: input.language.t("command.permissions.autoaccept.enable"),
|
||||
params.id && permission.isAutoAccepting(params.id, sdk.directory)
|
||||
? language.t("command.permissions.autoaccept.disable")
|
||||
: language.t("command.permissions.autoaccept.enable"),
|
||||
keybind: "mod+shift+a",
|
||||
disabled: !input.params.id || !input.permission.permissionsEnabled(),
|
||||
disabled: !params.id || !permission.permissionsEnabled(),
|
||||
onSelect: () => {
|
||||
const sessionID = input.params.id
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
input.permission.toggleAutoAccept(sessionID, input.sdk.directory)
|
||||
permission.toggleAutoAccept(sessionID, sdk.directory)
|
||||
showToast({
|
||||
title: input.permission.isAutoAccepting(sessionID, input.sdk.directory)
|
||||
? input.language.t("toast.permissions.autoaccept.on.title")
|
||||
: input.language.t("toast.permissions.autoaccept.off.title"),
|
||||
description: input.permission.isAutoAccepting(sessionID, input.sdk.directory)
|
||||
? input.language.t("toast.permissions.autoaccept.on.description")
|
||||
: input.language.t("toast.permissions.autoaccept.off.description"),
|
||||
title: permission.isAutoAccepting(sessionID, sdk.directory)
|
||||
? language.t("toast.permissions.autoaccept.on.title")
|
||||
: language.t("toast.permissions.autoaccept.off.title"),
|
||||
description: permission.isAutoAccepting(sessionID, sdk.directory)
|
||||
? language.t("toast.permissions.autoaccept.on.description")
|
||||
: language.t("toast.permissions.autoaccept.off.description"),
|
||||
})
|
||||
},
|
||||
}),
|
||||
@@ -257,71 +289,71 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
const sessionActionCommands = createMemo(() => [
|
||||
sessionCommand({
|
||||
id: "session.undo",
|
||||
title: input.language.t("command.session.undo"),
|
||||
description: input.language.t("command.session.undo.description"),
|
||||
title: language.t("command.session.undo"),
|
||||
description: language.t("command.session.undo.description"),
|
||||
slash: "undo",
|
||||
disabled: !input.params.id || input.visibleUserMessages().length === 0,
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = input.params.id
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
if (input.status()?.type !== "idle") {
|
||||
await input.sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
if (status()?.type !== "idle") {
|
||||
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
}
|
||||
const revert = input.info()?.revert?.messageID
|
||||
const message = findLast(input.userMessages(), (x) => !revert || x.id < revert)
|
||||
const revert = info()?.revert?.messageID
|
||||
const message = findLast(userMessages(), (x) => !revert || x.id < revert)
|
||||
if (!message) return
|
||||
await input.sdk.client.session.revert({ sessionID, messageID: message.id })
|
||||
const parts = input.sync.data.part[message.id]
|
||||
await sdk.client.session.revert({ sessionID, messageID: message.id })
|
||||
const parts = sync.data.part[message.id]
|
||||
if (parts) {
|
||||
const restored = extractPromptFromParts(parts, { directory: input.sdk.directory })
|
||||
input.prompt.set(restored)
|
||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
||||
prompt.set(restored)
|
||||
}
|
||||
const priorMessage = findLast(input.userMessages(), (x) => x.id < message.id)
|
||||
input.setActiveMessage(priorMessage)
|
||||
const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
|
||||
setActiveMessage(priorMessage)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.redo",
|
||||
title: input.language.t("command.session.redo"),
|
||||
description: input.language.t("command.session.redo.description"),
|
||||
title: language.t("command.session.redo"),
|
||||
description: language.t("command.session.redo.description"),
|
||||
slash: "redo",
|
||||
disabled: !input.params.id || !input.info()?.revert?.messageID,
|
||||
disabled: !params.id || !info()?.revert?.messageID,
|
||||
onSelect: async () => {
|
||||
const sessionID = input.params.id
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const revertMessageID = input.info()?.revert?.messageID
|
||||
const revertMessageID = info()?.revert?.messageID
|
||||
if (!revertMessageID) return
|
||||
const nextMessage = input.userMessages().find((x) => x.id > revertMessageID)
|
||||
const nextMessage = userMessages().find((x) => x.id > revertMessageID)
|
||||
if (!nextMessage) {
|
||||
await input.sdk.client.session.unrevert({ sessionID })
|
||||
input.prompt.reset()
|
||||
const lastMsg = findLast(input.userMessages(), (x) => x.id >= revertMessageID)
|
||||
input.setActiveMessage(lastMsg)
|
||||
await sdk.client.session.unrevert({ sessionID })
|
||||
prompt.reset()
|
||||
const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID)
|
||||
setActiveMessage(lastMsg)
|
||||
return
|
||||
}
|
||||
await input.sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
||||
const priorMsg = findLast(input.userMessages(), (x) => x.id < nextMessage.id)
|
||||
input.setActiveMessage(priorMsg)
|
||||
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
||||
const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
|
||||
setActiveMessage(priorMsg)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.compact",
|
||||
title: input.language.t("command.session.compact"),
|
||||
description: input.language.t("command.session.compact.description"),
|
||||
title: language.t("command.session.compact"),
|
||||
description: language.t("command.session.compact.description"),
|
||||
slash: "compact",
|
||||
disabled: !input.params.id || input.visibleUserMessages().length === 0,
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = input.params.id
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const model = input.local.model.current()
|
||||
const model = local.model.current()
|
||||
if (!model) {
|
||||
showToast({
|
||||
title: input.language.t("toast.model.none.title"),
|
||||
description: input.language.t("toast.model.none.description"),
|
||||
title: language.t("toast.model.none.title"),
|
||||
description: language.t("toast.model.none.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
await input.sdk.client.session.summarize({
|
||||
await sdk.client.session.summarize({
|
||||
sessionID,
|
||||
modelID: model.id,
|
||||
providerID: model.provider.id,
|
||||
@@ -330,29 +362,27 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.fork",
|
||||
title: input.language.t("command.session.fork"),
|
||||
description: input.language.t("command.session.fork.description"),
|
||||
title: language.t("command.session.fork"),
|
||||
description: language.t("command.session.fork.description"),
|
||||
slash: "fork",
|
||||
disabled: !input.params.id || input.visibleUserMessages().length === 0,
|
||||
onSelect: () => input.dialog.show(() => <DialogFork />),
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: () => dialog.show(() => <DialogFork />),
|
||||
}),
|
||||
])
|
||||
|
||||
const shareCommands = createMemo(() => {
|
||||
if (input.sync.data.config.share === "disabled") return []
|
||||
if (sync.data.config.share === "disabled") return []
|
||||
return [
|
||||
sessionCommand({
|
||||
id: "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
|
||||
? input.language.t("toast.session.share.success.description")
|
||||
: input.language.t("command.session.share.description"),
|
||||
title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"),
|
||||
description: info()?.share?.url
|
||||
? language.t("toast.session.share.success.description")
|
||||
: language.t("command.session.share.description"),
|
||||
slash: "share",
|
||||
disabled: !input.params.id,
|
||||
disabled: !params.id,
|
||||
onSelect: async () => {
|
||||
if (!input.params.id) return
|
||||
if (!params.id) return
|
||||
|
||||
const write = (value: string) => {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
@@ -382,7 +412,7 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
const ok = await write(url)
|
||||
if (!ok) {
|
||||
showToast({
|
||||
title: input.language.t("toast.session.share.copyFailed.title"),
|
||||
title: language.t("toast.session.share.copyFailed.title"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
@@ -390,27 +420,27 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
|
||||
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"),
|
||||
? language.t("session.share.copy.copied")
|
||||
: language.t("toast.session.share.success.title"),
|
||||
description: language.t("toast.session.share.success.description"),
|
||||
variant: "success",
|
||||
})
|
||||
}
|
||||
|
||||
const existing = input.info()?.share?.url
|
||||
const existing = info()?.share?.url
|
||||
if (existing) {
|
||||
await copy(existing, true)
|
||||
return
|
||||
}
|
||||
|
||||
const url = await input.sdk.client.session
|
||||
.share({ sessionID: input.params.id })
|
||||
const url = await sdk.client.session
|
||||
.share({ sessionID: params.id })
|
||||
.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"),
|
||||
title: language.t("toast.session.share.failed.title"),
|
||||
description: language.t("toast.session.share.failed.description"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
@@ -421,25 +451,25 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.unshare",
|
||||
title: input.language.t("command.session.unshare"),
|
||||
description: input.language.t("command.session.unshare.description"),
|
||||
title: language.t("command.session.unshare"),
|
||||
description: language.t("command.session.unshare.description"),
|
||||
slash: "unshare",
|
||||
disabled: !input.params.id || !input.info()?.share?.url,
|
||||
disabled: !params.id || !info()?.share?.url,
|
||||
onSelect: async () => {
|
||||
if (!input.params.id) return
|
||||
await input.sdk.client.session
|
||||
.unshare({ sessionID: input.params.id })
|
||||
if (!params.id) return
|
||||
await sdk.client.session
|
||||
.unshare({ sessionID: params.id })
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: input.language.t("toast.session.unshare.success.title"),
|
||||
description: input.language.t("toast.session.unshare.success.description"),
|
||||
title: language.t("toast.session.unshare.success.title"),
|
||||
description: language.t("toast.session.unshare.success.description"),
|
||||
variant: "success",
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: input.language.t("toast.session.unshare.failed.title"),
|
||||
description: input.language.t("toast.session.unshare.failed.description"),
|
||||
title: language.t("toast.session.unshare.failed.title"),
|
||||
description: language.t("toast.session.unshare.failed.description"),
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
@@ -448,7 +478,7 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
]
|
||||
})
|
||||
|
||||
input.command.register("session", () =>
|
||||
command.register("session", () =>
|
||||
[
|
||||
sessionCommands(),
|
||||
fileCommands(),
|
||||
|
||||
2
packages/app/src/sst-env.d.ts
vendored
2
packages/app/src/sst-env.d.ts
vendored
@@ -1,6 +1,8 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {
|
||||
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import { uuid } from "@/utils/uuid"
|
||||
|
||||
type Nav = {
|
||||
id: string
|
||||
dir?: string
|
||||
from?: string
|
||||
to: string
|
||||
trigger?: string
|
||||
start: number
|
||||
marks: Record<string, number>
|
||||
logged: boolean
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
const dev = import.meta.env.DEV
|
||||
|
||||
const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`
|
||||
|
||||
const now = () => performance.now()
|
||||
|
||||
const navs = new Map<string, Nav>()
|
||||
const pending = new Map<string, string>()
|
||||
const active = new Map<string, string>()
|
||||
|
||||
const required = [
|
||||
"session:params",
|
||||
"session:data-ready",
|
||||
"session:first-turn-mounted",
|
||||
"storage:prompt-ready",
|
||||
"storage:terminal-ready",
|
||||
"storage:file-view-ready",
|
||||
]
|
||||
|
||||
function flush(id: string, reason: "complete" | "timeout") {
|
||||
if (!dev) return
|
||||
const nav = navs.get(id)
|
||||
if (!nav) return
|
||||
if (nav.logged) return
|
||||
|
||||
nav.logged = true
|
||||
if (nav.timer) clearTimeout(nav.timer)
|
||||
|
||||
const baseName = nav.marks["navigate:start"] !== undefined ? "navigate:start" : "session:params"
|
||||
const base = nav.marks[baseName] ?? nav.start
|
||||
|
||||
const ms = Object.fromEntries(
|
||||
Object.entries(nav.marks)
|
||||
.slice()
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([name, t]) => [name, Math.round((t - base) * 100) / 100]),
|
||||
)
|
||||
|
||||
console.log(
|
||||
"perf.session-nav " +
|
||||
JSON.stringify({
|
||||
type: "perf.session-nav.v0",
|
||||
id: nav.id,
|
||||
dir: nav.dir,
|
||||
from: nav.from,
|
||||
to: nav.to,
|
||||
trigger: nav.trigger,
|
||||
base: baseName,
|
||||
reason,
|
||||
ms,
|
||||
}),
|
||||
)
|
||||
|
||||
navs.delete(id)
|
||||
}
|
||||
|
||||
function maybeFlush(id: string) {
|
||||
if (!dev) return
|
||||
const nav = navs.get(id)
|
||||
if (!nav) return
|
||||
if (nav.logged) return
|
||||
if (!required.every((name) => nav.marks[name] !== undefined)) return
|
||||
flush(id, "complete")
|
||||
}
|
||||
|
||||
function ensure(id: string, data: Omit<Nav, "marks" | "logged" | "timer">) {
|
||||
const existing = navs.get(id)
|
||||
if (existing) return existing
|
||||
|
||||
const nav: Nav = {
|
||||
...data,
|
||||
marks: {},
|
||||
logged: false,
|
||||
}
|
||||
nav.timer = setTimeout(() => flush(id, "timeout"), 5000)
|
||||
navs.set(id, nav)
|
||||
return nav
|
||||
}
|
||||
|
||||
export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) {
|
||||
if (!dev) return
|
||||
|
||||
const id = uuid()
|
||||
const start = now()
|
||||
const nav = ensure(id, { ...input, id, start })
|
||||
nav.marks["navigate:start"] = start
|
||||
|
||||
pending.set(key(input.dir, input.to), id)
|
||||
return id
|
||||
}
|
||||
|
||||
export function navParams(input: { dir?: string; from?: string; to: string }) {
|
||||
if (!dev) return
|
||||
|
||||
const k = key(input.dir, input.to)
|
||||
const pendingId = pending.get(k)
|
||||
if (pendingId) pending.delete(k)
|
||||
const id = pendingId ?? uuid()
|
||||
|
||||
const start = now()
|
||||
const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })
|
||||
nav.marks["session:params"] = start
|
||||
|
||||
active.set(k, id)
|
||||
maybeFlush(id)
|
||||
return id
|
||||
}
|
||||
|
||||
export function navMark(input: { dir?: string; to: string; name: string }) {
|
||||
if (!dev) return
|
||||
|
||||
const id = active.get(key(input.dir, input.to))
|
||||
if (!id) return
|
||||
|
||||
const nav = navs.get(id)
|
||||
if (!nav) return
|
||||
if (nav.marks[input.name] !== undefined) return
|
||||
|
||||
nav.marks[input.name] = now()
|
||||
maybeFlush(id)
|
||||
}
|
||||
1
packages/app/sst-env.d.ts
vendored
1
packages/app/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"dev": "vite dev --host 0.0.0.0",
|
||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev",
|
||||
"build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||
"build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json ./.output/public/tui.json",
|
||||
"start": "vite start"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
1
packages/console/app/sst-env.d.ts
vendored
1
packages/console/app/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../../sst-env.d.ts" />
|
||||
|
||||
|
||||
1
packages/console/core/sst-env.d.ts
vendored
1
packages/console/core/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
|
||||
1
packages/console/function/sst-env.d.ts
vendored
1
packages/console/function/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
|
||||
1
packages/console/mail/sst-env.d.ts
vendored
1
packages/console/mail/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../../sst-env.d.ts" />
|
||||
|
||||
|
||||
1
packages/console/resource/sst-env.d.ts
vendored
1
packages/console/resource/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
|
||||
1
packages/desktop/src-tauri/Cargo.lock
generated
1
packages/desktop/src-tauri/Cargo.lock
generated
@@ -3136,6 +3136,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"webkit2gtk",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -54,6 +54,9 @@ chrono = "0.4"
|
||||
tokio-stream = { version = "0.1.18", features = ["sync"] }
|
||||
process-wrap = { version = "9.0.3", features = ["tokio1"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62", features = ["Win32_System_Threading"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gtk = "0.18.2"
|
||||
webkit2gtk = "=2.0.2"
|
||||
|
||||
@@ -3,7 +3,7 @@ use process_wrap::tokio::CommandWrap;
|
||||
#[cfg(unix)]
|
||||
use process_wrap::tokio::ProcessGroup;
|
||||
#[cfg(windows)]
|
||||
use process_wrap::tokio::{JobObject, KillOnDrop};
|
||||
use process_wrap::tokio::{CommandWrapper, JobObject, KillOnDrop};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::sync::Arc;
|
||||
@@ -18,9 +18,24 @@ use tokio::{
|
||||
};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tracing::Instrument;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED};
|
||||
|
||||
use crate::server::get_wsl_config;
|
||||
|
||||
#[cfg(windows)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
// Keep this as a custom wrapper instead of process_wrap::CreationFlags.
|
||||
// JobObject pre_spawn rewrites creation flags, so this must run after it.
|
||||
struct WinCreationFlags;
|
||||
|
||||
#[cfg(windows)]
|
||||
impl CommandWrapper for WinCreationFlags {
|
||||
fn pre_spawn(&mut self, command: &mut Command, _core: &CommandWrap) -> std::io::Result<()> {
|
||||
command.creation_flags((CREATE_NO_WINDOW | CREATE_SUSPENDED).0);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const CLI_INSTALL_DIR: &str = ".opencode/bin";
|
||||
const CLI_BINARY_NAME: &str = "opencode";
|
||||
@@ -203,7 +218,7 @@ fn get_user_shell() -> String {
|
||||
}
|
||||
|
||||
fn is_wsl_enabled(_app: &tauri::AppHandle) -> bool {
|
||||
get_wsl_config(_app.clone()).is_ok_and(|v| v.enabled)
|
||||
get_wsl_config(_app.clone()).is_ok_and(|v| v.enabled)
|
||||
}
|
||||
|
||||
fn shell_escape(input: &str) -> String {
|
||||
@@ -318,9 +333,6 @@ pub fn spawn_command(
|
||||
cmd.stderr(Stdio::piped());
|
||||
cmd.stdin(Stdio::null());
|
||||
|
||||
#[cfg(windows)]
|
||||
cmd.creation_flags(0x0800_0000);
|
||||
|
||||
let mut wrap = CommandWrap::from(cmd);
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -330,7 +342,7 @@ pub fn spawn_command(
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
wrap.wrap(JobObject).wrap(KillOnDrop);
|
||||
wrap.wrap(JobObject).wrap(WinCreationFlags).wrap(KillOnDrop);
|
||||
}
|
||||
|
||||
let mut child = wrap.spawn()?;
|
||||
|
||||
1
packages/desktop/sst-env.d.ts
vendored
1
packages/desktop/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
|
||||
1
packages/enterprise/sst-env.d.ts
vendored
1
packages/enterprise/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
|
||||
1
packages/function/sst-env.d.ts
vendored
1
packages/function/sst-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
|
||||
@@ -2,46 +2,62 @@
|
||||
|
||||
import { z } from "zod"
|
||||
import { Config } from "../src/config/config"
|
||||
import { TuiConfig } from "../src/config/tui"
|
||||
|
||||
const file = process.argv[2]
|
||||
console.log(file)
|
||||
function generate(schema: z.ZodType) {
|
||||
const result = z.toJSONSchema(schema, {
|
||||
io: "input", // Generate input shape (treats optional().default() as not required)
|
||||
/**
|
||||
* We'll use the `default` values of the field as the only value in `examples`.
|
||||
* This will ensure no docs are needed to be read, as the configuration is
|
||||
* self-documenting.
|
||||
*
|
||||
* See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5
|
||||
*/
|
||||
override(ctx) {
|
||||
const schema = ctx.jsonSchema
|
||||
|
||||
const result = z.toJSONSchema(Config.Info, {
|
||||
io: "input", // Generate input shape (treats optional().default() as not required)
|
||||
/**
|
||||
* We'll use the `default` values of the field as the only value in `examples`.
|
||||
* This will ensure no docs are needed to be read, as the configuration is
|
||||
* self-documenting.
|
||||
*
|
||||
* See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5
|
||||
*/
|
||||
override(ctx) {
|
||||
const schema = ctx.jsonSchema
|
||||
|
||||
// Preserve strictness: set additionalProperties: false for objects
|
||||
if (schema && typeof schema === "object" && schema.type === "object" && schema.additionalProperties === undefined) {
|
||||
schema.additionalProperties = false
|
||||
}
|
||||
|
||||
// Add examples and default descriptions for string fields with defaults
|
||||
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
|
||||
if (!schema.examples) {
|
||||
schema.examples = [schema.default]
|
||||
// Preserve strictness: set additionalProperties: false for objects
|
||||
if (
|
||||
schema &&
|
||||
typeof schema === "object" &&
|
||||
schema.type === "object" &&
|
||||
schema.additionalProperties === undefined
|
||||
) {
|
||||
schema.additionalProperties = false
|
||||
}
|
||||
|
||||
schema.description = [schema.description || "", `default: \`${schema.default}\``]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
.trim()
|
||||
}
|
||||
},
|
||||
}) as Record<string, unknown> & {
|
||||
allowComments?: boolean
|
||||
allowTrailingCommas?: boolean
|
||||
// Add examples and default descriptions for string fields with defaults
|
||||
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
|
||||
if (!schema.examples) {
|
||||
schema.examples = [schema.default]
|
||||
}
|
||||
|
||||
schema.description = [schema.description || "", `default: \`${schema.default}\``]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
.trim()
|
||||
}
|
||||
},
|
||||
}) as Record<string, unknown> & {
|
||||
allowComments?: boolean
|
||||
allowTrailingCommas?: boolean
|
||||
}
|
||||
|
||||
// used for json lsps since config supports jsonc
|
||||
result.allowComments = true
|
||||
result.allowTrailingCommas = true
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// used for json lsps since config supports jsonc
|
||||
result.allowComments = true
|
||||
result.allowTrailingCommas = true
|
||||
const configFile = process.argv[2]
|
||||
const tuiFile = process.argv[3]
|
||||
|
||||
await Bun.write(file, JSON.stringify(result, null, 2))
|
||||
console.log(configFile)
|
||||
await Bun.write(configFile, JSON.stringify(generate(Config.Info), null, 2))
|
||||
|
||||
if (tuiFile) {
|
||||
console.log(tuiFile)
|
||||
await Bun.write(tuiFile, JSON.stringify(generate(TuiConfig.Info), null, 2))
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
|
||||
import { Log } from "../util/log"
|
||||
import { pathToFileURL } from "bun"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { ACPSessionManager } from "./session"
|
||||
import type { ACPConfig } from "./types"
|
||||
import { Provider } from "../provider/provider"
|
||||
@@ -228,8 +229,7 @@ export namespace ACP {
|
||||
const metadata = permission.metadata || {}
|
||||
const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
|
||||
const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
|
||||
const file = Bun.file(filepath)
|
||||
const content = (await file.exists()) ? await file.text() : ""
|
||||
const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : ""
|
||||
const newContent = getNewContent(content, diff)
|
||||
|
||||
if (newContent) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from "path"
|
||||
import { Global } from "../global"
|
||||
import z from "zod"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
|
||||
|
||||
@@ -42,8 +43,7 @@ export namespace Auth {
|
||||
}
|
||||
|
||||
export async function all(): Promise<Record<string, Info>> {
|
||||
const file = Bun.file(filepath)
|
||||
const data = await file.json().catch(() => ({}) as Record<string, unknown>)
|
||||
const data = await Filesystem.readJson<Record<string, unknown>>(filepath).catch(() => ({}))
|
||||
return Object.entries(data).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const parsed = Info.safeParse(value)
|
||||
@@ -56,15 +56,13 @@ export namespace Auth {
|
||||
}
|
||||
|
||||
export async function set(key: string, info: Info) {
|
||||
const file = Bun.file(filepath)
|
||||
const data = await all()
|
||||
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2), { mode: 0o600 })
|
||||
await Filesystem.writeJson(filepath, { ...data, [key]: info }, 0o600)
|
||||
}
|
||||
|
||||
export async function remove(key: string) {
|
||||
const file = Bun.file(filepath)
|
||||
const data = await all()
|
||||
delete data[key]
|
||||
await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
|
||||
await Filesystem.writeJson(filepath, data, 0o600)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,14 +66,14 @@ export namespace BunProc {
|
||||
using _ = await Lock.write("bun-install")
|
||||
|
||||
const mod = path.join(Global.Path.cache, "node_modules", pkg)
|
||||
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
||||
const parsed = await pkgjson.json().catch(async () => {
|
||||
const result = { dependencies: {} }
|
||||
await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
|
||||
const pkgjsonPath = path.join(Global.Path.cache, "package.json")
|
||||
const parsed = await Filesystem.readJson<{ dependencies: Record<string, string> }>(pkgjsonPath).catch(async () => {
|
||||
const result = { dependencies: {} as Record<string, string> }
|
||||
await Filesystem.writeJson(pkgjsonPath, result)
|
||||
return result
|
||||
})
|
||||
const dependencies = parsed.dependencies ?? {}
|
||||
if (!parsed.dependencies) parsed.dependencies = dependencies
|
||||
if (!parsed.dependencies) parsed.dependencies = {} as Record<string, string>
|
||||
const dependencies = parsed.dependencies
|
||||
const modExists = await Filesystem.exists(mod)
|
||||
const cachedVersion = dependencies[pkg]
|
||||
|
||||
@@ -123,15 +123,16 @@ export namespace BunProc {
|
||||
// This ensures subsequent starts use the cached version until explicitly updated
|
||||
let resolvedVersion = version
|
||||
if (version === "latest") {
|
||||
const installedPkgJson = Bun.file(path.join(mod, "package.json"))
|
||||
const installedPkg = await installedPkgJson.json().catch(() => null)
|
||||
const installedPkg = await Filesystem.readJson<{ version?: string }>(path.join(mod, "package.json")).catch(
|
||||
() => null,
|
||||
)
|
||||
if (installedPkg?.version) {
|
||||
resolvedVersion = installedPkg.version
|
||||
}
|
||||
}
|
||||
|
||||
parsed.dependencies[pkg] = resolvedVersion
|
||||
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
|
||||
await Filesystem.writeJson(pkgjsonPath, parsed)
|
||||
return mod
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Agent } from "../../agent/agent"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import matter from "gray-matter"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { EOL } from "os"
|
||||
@@ -202,8 +203,7 @@ const AgentCreateCommand = cmd({
|
||||
|
||||
await fs.mkdir(targetPath, { recursive: true })
|
||||
|
||||
const file = Bun.file(filePath)
|
||||
if (await file.exists()) {
|
||||
if (await Filesystem.exists(filePath)) {
|
||||
if (isFullyNonInteractive) {
|
||||
console.error(`Error: Agent file already exists: ${filePath}`)
|
||||
process.exit(1)
|
||||
@@ -212,7 +212,7 @@ const AgentCreateCommand = cmd({
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
|
||||
await Bun.write(filePath, content)
|
||||
await Filesystem.write(filePath, content)
|
||||
|
||||
if (isFullyNonInteractive) {
|
||||
console.log(filePath)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from "path"
|
||||
import { exec } from "child_process"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import { Octokit } from "@octokit/rest"
|
||||
@@ -173,6 +174,18 @@ export function extractResponseText(parts: MessageV2.Part[]): string | null {
|
||||
throw new Error("Failed to parse response: no parts returned")
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a PROMPT_TOO_LARGE error message with details about files in the prompt.
|
||||
* Content is base64 encoded, so we calculate original size by multiplying by 0.75.
|
||||
*/
|
||||
export function formatPromptTooLargeError(files: { filename: string; content: string }[]): string {
|
||||
const fileDetails =
|
||||
files.length > 0
|
||||
? `\n\nFiles in prompt:\n${files.map((f) => ` - ${f.filename} (${((f.content.length * 0.75) / 1024).toFixed(0)} KB)`).join("\n")}`
|
||||
: ""
|
||||
return `PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.${fileDetails}`
|
||||
}
|
||||
|
||||
export const GithubCommand = cmd({
|
||||
command: "github",
|
||||
describe: "manage GitHub agent",
|
||||
@@ -360,7 +373,7 @@ export const GithubInstallCommand = cmd({
|
||||
? ""
|
||||
: `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
|
||||
|
||||
await Bun.write(
|
||||
await Filesystem.write(
|
||||
path.join(app.root, WORKFLOW_FILE),
|
||||
`name: opencode
|
||||
|
||||
@@ -802,6 +815,7 @@ export const GithubRunCommand = cmd({
|
||||
replacement,
|
||||
})
|
||||
}
|
||||
|
||||
return { userPrompt: prompt, promptFiles: imgData }
|
||||
}
|
||||
|
||||
@@ -909,10 +923,15 @@ export const GithubRunCommand = cmd({
|
||||
|
||||
// result should always be assistant just satisfying type checker
|
||||
if (result.info.role === "assistant" && result.info.error) {
|
||||
console.error("Agent error:", result.info.error)
|
||||
throw new Error(
|
||||
`${result.info.error.name}: ${"message" in result.info.error ? result.info.error.message : ""}`,
|
||||
)
|
||||
const err = result.info.error
|
||||
console.error("Agent error:", err)
|
||||
|
||||
if (err.name === "ContextOverflowError") {
|
||||
throw new Error(formatPromptTooLargeError(files))
|
||||
}
|
||||
|
||||
const errorMsg = err.data?.message || ""
|
||||
throw new Error(`${err.name}: ${errorMsg}`)
|
||||
}
|
||||
|
||||
const text = extractResponseText(result.parts)
|
||||
@@ -938,10 +957,15 @@ export const GithubRunCommand = cmd({
|
||||
})
|
||||
|
||||
if (summary.info.role === "assistant" && summary.info.error) {
|
||||
console.error("Summary agent error:", summary.info.error)
|
||||
throw new Error(
|
||||
`${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`,
|
||||
)
|
||||
const err = summary.info.error
|
||||
console.error("Summary agent error:", err)
|
||||
|
||||
if (err.name === "ContextOverflowError") {
|
||||
throw new Error(formatPromptTooLargeError(files))
|
||||
}
|
||||
|
||||
const errorMsg = err.data?.message || ""
|
||||
throw new Error(`${err.name}: ${errorMsg}`)
|
||||
}
|
||||
|
||||
const summaryText = extractResponseText(summary.parts)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SessionTable, MessageTable, PartTable } from "../../session/session.sql
|
||||
import { Instance } from "../../project/instance"
|
||||
import { ShareNext } from "../../share/share-next"
|
||||
import { EOL } from "os"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
|
||||
/** Discriminated union returned by the ShareNext API (GET /api/share/:id/data) */
|
||||
export type ShareData =
|
||||
@@ -116,8 +117,7 @@ export const ImportCommand = cmd({
|
||||
|
||||
exportData = transformed
|
||||
} else {
|
||||
const file = Bun.file(args.file)
|
||||
exportData = await file.json().catch(() => {})
|
||||
exportData = await Filesystem.readJson<NonNullable<typeof exportData>>(args.file).catch(() => undefined)
|
||||
if (!exportData) {
|
||||
process.stdout.write(`File not found: ${args.file}`)
|
||||
process.stdout.write(EOL)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { cmd } from "./cmd"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { EOL } from "os"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { Server } from "../../server/server"
|
||||
import { Provider } from "../../provider/provider"
|
||||
@@ -315,19 +316,12 @@ export const RunCommand = cmd({
|
||||
|
||||
for (const filePath of list) {
|
||||
const resolvedPath = path.resolve(process.cwd(), filePath)
|
||||
const file = Bun.file(resolvedPath)
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) {
|
||||
UI.error(`File not found: ${filePath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (!(await file.exists())) {
|
||||
if (!(await Filesystem.exists(resolvedPath))) {
|
||||
UI.error(`File not found: ${filePath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const stat = await file.stat()
|
||||
const mime = stat.isDirectory() ? "application/x-directory" : "text/plain"
|
||||
const mime = (await Filesystem.isDir(resolvedPath)) ? "application/x-directory" : "text/plain"
|
||||
|
||||
files.push({
|
||||
type: "file",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { bootstrap } from "../bootstrap"
|
||||
import { UI } from "../ui"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { EOL } from "os"
|
||||
import path from "path"
|
||||
|
||||
@@ -17,18 +18,18 @@ function pagerCmd(): string[] {
|
||||
// user could have less installed via other options
|
||||
const lessOnPath = Bun.which("less")
|
||||
if (lessOnPath) {
|
||||
if (Bun.file(lessOnPath).size) return [lessOnPath, ...lessOptions]
|
||||
if (Filesystem.stat(lessOnPath)?.size) return [lessOnPath, ...lessOptions]
|
||||
}
|
||||
|
||||
if (Flag.OPENCODE_GIT_BASH_PATH) {
|
||||
const less = path.join(Flag.OPENCODE_GIT_BASH_PATH, "..", "..", "usr", "bin", "less.exe")
|
||||
if (Bun.file(less).size) return [less, ...lessOptions]
|
||||
if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
|
||||
}
|
||||
|
||||
const git = Bun.which("git")
|
||||
if (git) {
|
||||
const less = path.join(git, "..", "..", "usr", "bin", "less.exe")
|
||||
if (Bun.file(less).size) return [less, ...lessOptions]
|
||||
if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
|
||||
}
|
||||
|
||||
// Fall back to Windows built-in more (via cmd.exe)
|
||||
|
||||
@@ -38,6 +38,8 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
|
||||
import open from "open"
|
||||
import { writeHeapSnapshot } from "v8"
|
||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||
import { TuiConfigProvider } from "./context/tui-config"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
|
||||
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
// can't set raw mode if not a TTY
|
||||
@@ -104,6 +106,7 @@ import type { EventSource } from "./context/sdk"
|
||||
export function tui(input: {
|
||||
url: string
|
||||
args: Args
|
||||
config: TuiConfig.Info
|
||||
directory?: string
|
||||
fetch?: typeof fetch
|
||||
headers?: RequestInit["headers"]
|
||||
@@ -138,35 +141,37 @@ export function tui(input: {
|
||||
<KVProvider>
|
||||
<ToastProvider>
|
||||
<RouteProvider>
|
||||
<SDKProvider
|
||||
url={input.url}
|
||||
directory={input.directory}
|
||||
fetch={input.fetch}
|
||||
headers={input.headers}
|
||||
events={input.events}
|
||||
>
|
||||
<SyncProvider>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
<KeybindProvider>
|
||||
<PromptStashProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<App />
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</PromptStashProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
<TuiConfigProvider config={input.config}>
|
||||
<SDKProvider
|
||||
url={input.url}
|
||||
directory={input.directory}
|
||||
fetch={input.fetch}
|
||||
headers={input.headers}
|
||||
events={input.events}
|
||||
>
|
||||
<SyncProvider>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
<KeybindProvider>
|
||||
<PromptStashProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<App />
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</PromptStashProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
</TuiConfigProvider>
|
||||
</RouteProvider>
|
||||
</ToastProvider>
|
||||
</KVProvider>
|
||||
|
||||
@@ -2,6 +2,9 @@ import { cmd } from "../cmd"
|
||||
import { UI } from "@/cli/ui"
|
||||
import { tui } from "./app"
|
||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { existsSync } from "fs"
|
||||
|
||||
export const AttachCommand = cmd({
|
||||
command: "attach <url>",
|
||||
@@ -63,8 +66,13 @@ export const AttachCommand = cmd({
|
||||
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
|
||||
return { Authorization: auth }
|
||||
})()
|
||||
const config = await Instance.provide({
|
||||
directory: directory && existsSync(directory) ? directory : process.cwd(),
|
||||
fn: () => TuiConfig.get(),
|
||||
})
|
||||
await tui({
|
||||
url: args.url,
|
||||
config,
|
||||
args: {
|
||||
continue: args.continue,
|
||||
sessionID: args.session,
|
||||
|
||||
@@ -694,7 +694,7 @@ export function Prompt(props: PromptProps) {
|
||||
async function pasteImage(file: { filename?: string; content: string; mime: string }) {
|
||||
const currentOffset = input.visualCursor.offset
|
||||
const extmarkStart = currentOffset
|
||||
const count = store.prompt.parts.filter((x) => x.type === "file").length
|
||||
const count = store.prompt.parts.filter((x) => x.type === "file" && x.mime.startsWith("image/")).length
|
||||
const virtualText = `[Image ${count + 1}]`
|
||||
const extmarkEnd = extmarkStart + virtualText.length
|
||||
const textToInsert = virtualText + " "
|
||||
|
||||
@@ -80,11 +80,11 @@ const TIPS = [
|
||||
"Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes",
|
||||
"Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents",
|
||||
"Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions",
|
||||
"Create {highlight}opencode.json{/highlight} in project root for project-specific settings",
|
||||
"Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config",
|
||||
"Create {highlight}opencode.json{/highlight} for server settings and {highlight}tui.json{/highlight} for TUI settings",
|
||||
"Place TUI settings in {highlight}~/.config/opencode/tui.json{/highlight} for global config",
|
||||
"Add {highlight}$schema{/highlight} to your config for autocomplete in your editor",
|
||||
"Configure {highlight}model{/highlight} in config to set your default model",
|
||||
"Override any keybind in config via the {highlight}keybinds{/highlight} section",
|
||||
"Override any keybind in {highlight}tui.json{/highlight} via the {highlight}keybinds{/highlight} section",
|
||||
"Set any keybind to {highlight}none{/highlight} to disable it completely",
|
||||
"Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section",
|
||||
"OpenCode auto-handles OAuth for remote MCP servers requiring auth",
|
||||
@@ -140,7 +140,7 @@ const TIPS = [
|
||||
"Press {highlight}Ctrl+X G{/highlight} or {highlight}/timeline{/highlight} to jump to specific messages",
|
||||
"Press {highlight}Ctrl+X H{/highlight} to toggle code block visibility in messages",
|
||||
"Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info",
|
||||
"Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling",
|
||||
"Enable {highlight}scroll_acceleration{/highlight} in {highlight}tui.json{/highlight} for smooth macOS-style scrolling",
|
||||
"Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight})",
|
||||
"Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use",
|
||||
"Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { pipe, mapValues } from "remeda"
|
||||
import type { KeybindsConfig } from "@opencode-ai/sdk/v2"
|
||||
@@ -7,14 +6,15 @@ import type { ParsedKey, Renderable } from "@opentui/core"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useKeyboard, useRenderer } from "@opentui/solid"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useTuiConfig } from "./tui-config"
|
||||
|
||||
export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({
|
||||
name: "Keybind",
|
||||
init: () => {
|
||||
const sync = useSync()
|
||||
const keybinds = createMemo(() => {
|
||||
const config = useTuiConfig()
|
||||
const keybinds = createMemo<Record<string, Keybind.Info[]>>(() => {
|
||||
return pipe(
|
||||
sync.data.config.keybinds ?? {},
|
||||
(config.keybinds ?? {}) as Record<string, string>,
|
||||
mapValues((value) => Keybind.parse(value)),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
|
||||
import path from "path"
|
||||
import { createEffect, createMemo, onMount } from "solid-js"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import aura from "./theme/aura.json" with { type: "json" }
|
||||
import ayu from "./theme/ayu.json" with { type: "json" }
|
||||
@@ -41,6 +40,7 @@ import { useRenderer } from "@opentui/solid"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { useTuiConfig } from "./tui-config"
|
||||
|
||||
type ThemeColors = {
|
||||
primary: RGBA
|
||||
@@ -279,17 +279,17 @@ function ansiToRgba(code: number): RGBA {
|
||||
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
name: "Theme",
|
||||
init: (props: { mode: "dark" | "light" }) => {
|
||||
const sync = useSync()
|
||||
const config = useTuiConfig()
|
||||
const kv = useKV()
|
||||
const [store, setStore] = createStore({
|
||||
themes: DEFAULT_THEMES,
|
||||
mode: kv.get("theme_mode", props.mode),
|
||||
active: (sync.data.config.theme ?? kv.get("theme", "opencode")) as string,
|
||||
active: (config.theme ?? kv.get("theme", "opencode")) as string,
|
||||
ready: false,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const theme = sync.data.config.theme
|
||||
const theme = config.theme
|
||||
if (theme) setStore("active", theme)
|
||||
})
|
||||
|
||||
|
||||
9
packages/opencode/src/cli/cmd/tui/context/tui-config.tsx
Normal file
9
packages/opencode/src/cli/cmd/tui/context/tui-config.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
export const { use: useTuiConfig, provider: TuiConfigProvider } = createSimpleContext({
|
||||
name: "TuiConfig",
|
||||
init: (props: { config: TuiConfig.Info }) => {
|
||||
return props.config
|
||||
},
|
||||
})
|
||||
@@ -78,6 +78,7 @@ import { QuestionPrompt } from "./question"
|
||||
import { DialogExportOptions } from "../../ui/dialog-export-options"
|
||||
import { formatTranscript } from "../../util/transcript"
|
||||
import { UI } from "@/cli/ui.ts"
|
||||
import { useTuiConfig } from "../../context/tui-config"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -100,6 +101,7 @@ const context = createContext<{
|
||||
showDetails: () => boolean
|
||||
diffWrapMode: () => "word" | "none"
|
||||
sync: ReturnType<typeof useSync>
|
||||
tui: ReturnType<typeof useTuiConfig>
|
||||
}>()
|
||||
|
||||
function use() {
|
||||
@@ -112,6 +114,7 @@ export function Session() {
|
||||
const route = useRouteData("session")
|
||||
const { navigate } = useRoute()
|
||||
const sync = useSync()
|
||||
const tuiConfig = useTuiConfig()
|
||||
const kv = useKV()
|
||||
const { theme } = useTheme()
|
||||
const promptRef = usePromptRef()
|
||||
@@ -164,7 +167,7 @@ export function Session() {
|
||||
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
|
||||
|
||||
const scrollAcceleration = createMemo(() => {
|
||||
const tui = sync.data.config.tui
|
||||
const tui = tuiConfig
|
||||
if (tui?.scroll_acceleration?.enabled) {
|
||||
return new MacOSScrollAccel()
|
||||
}
|
||||
@@ -976,6 +979,7 @@ export function Session() {
|
||||
showDetails,
|
||||
diffWrapMode,
|
||||
sync,
|
||||
tui: tuiConfig,
|
||||
}}
|
||||
>
|
||||
<box flexDirection="row">
|
||||
@@ -1920,7 +1924,7 @@ function Edit(props: ToolProps<typeof EditTool>) {
|
||||
const { theme, syntax } = useTheme()
|
||||
|
||||
const view = createMemo(() => {
|
||||
const diffStyle = ctx.sync.data.config.tui?.diff_style
|
||||
const diffStyle = ctx.tui.diff_style
|
||||
if (diffStyle === "stacked") return "unified"
|
||||
// Default to "auto" behavior
|
||||
return ctx.width > 120 ? "split" : "unified"
|
||||
@@ -1991,7 +1995,7 @@ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
|
||||
const files = createMemo(() => props.metadata.files ?? [])
|
||||
|
||||
const view = createMemo(() => {
|
||||
const diffStyle = ctx.sync.data.config.tui?.diff_style
|
||||
const diffStyle = ctx.tui.diff_style
|
||||
if (diffStyle === "stacked") return "unified"
|
||||
return ctx.width > 120 ? "split" : "unified"
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Keybind } from "@/util/keybind"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { Global } from "@/global"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { useTuiConfig } from "../../context/tui-config"
|
||||
|
||||
type PermissionStage = "permission" | "always" | "reject"
|
||||
|
||||
@@ -48,14 +49,14 @@ function EditBody(props: { request: PermissionRequest }) {
|
||||
const themeState = useTheme()
|
||||
const theme = themeState.theme
|
||||
const syntax = themeState.syntax
|
||||
const sync = useSync()
|
||||
const config = useTuiConfig()
|
||||
const dimensions = useTerminalDimensions()
|
||||
|
||||
const filepath = createMemo(() => (props.request.metadata?.filepath as string) ?? "")
|
||||
const diff = createMemo(() => (props.request.metadata?.diff as string) ?? "")
|
||||
|
||||
const view = createMemo(() => {
|
||||
const diffStyle = sync.data.config.tui?.diff_style
|
||||
const diffStyle = config.diff_style
|
||||
if (diffStyle === "stacked") return "unified"
|
||||
return dimensions().width > 120 ? "split" : "unified"
|
||||
})
|
||||
|
||||
@@ -3,13 +3,17 @@ import { tui } from "./app"
|
||||
import { Rpc } from "@/util/rpc"
|
||||
import { type rpc } from "./worker"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { UI } from "@/cli/ui"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Log } from "@/util/log"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import type { Event } from "@opencode-ai/sdk/v2"
|
||||
import type { EventSource } from "./context/sdk"
|
||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { Instance } from "@/project/instance"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_WORKER_PATH: string
|
||||
@@ -99,7 +103,7 @@ export const TuiThreadCommand = cmd({
|
||||
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
|
||||
const workerPath = await iife(async () => {
|
||||
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
|
||||
if (await Bun.file(distWorker).exists()) return distWorker
|
||||
if (await Filesystem.exists(fileURLToPath(distWorker))) return distWorker
|
||||
return localWorker
|
||||
})
|
||||
try {
|
||||
@@ -133,6 +137,10 @@ export const TuiThreadCommand = cmd({
|
||||
if (!args.prompt) return piped
|
||||
return piped ? piped + "\n" + args.prompt : args.prompt
|
||||
})
|
||||
const config = await Instance.provide({
|
||||
directory: cwd,
|
||||
fn: () => TuiConfig.get(),
|
||||
})
|
||||
|
||||
// Check if server should be started (port or hostname explicitly set in CLI or config)
|
||||
const networkOpts = await resolveNetworkOptions(args)
|
||||
@@ -161,6 +169,8 @@ export const TuiThreadCommand = cmd({
|
||||
|
||||
const tuiPromise = tui({
|
||||
url,
|
||||
config,
|
||||
directory: cwd,
|
||||
fetch: customFetch,
|
||||
events,
|
||||
args: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import clipboardy from "clipboardy"
|
||||
import { lazy } from "../../../../util/lazy.js"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import { Filesystem } from "../../../../util/filesystem"
|
||||
|
||||
/**
|
||||
* Writes text to clipboard via OSC 52 escape sequence.
|
||||
@@ -34,9 +35,8 @@ export namespace Clipboard {
|
||||
await $`osascript -e 'set imageData to the clipboard as "PNGf"' -e 'set fileRef to open for access POSIX file "${tmpfile}" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
const file = Bun.file(tmpfile)
|
||||
const buffer = await file.arrayBuffer()
|
||||
return { data: Buffer.from(buffer).toString("base64"), mime: "image/png" }
|
||||
const buffer = await Filesystem.readBytes(tmpfile)
|
||||
return { data: buffer.toString("base64"), mime: "image/png" }
|
||||
} catch {
|
||||
} finally {
|
||||
await $`rm -f "${tmpfile}"`.nothrow().quiet()
|
||||
|
||||
@@ -3,6 +3,7 @@ import { rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { CliRenderer } from "@opentui/core"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
export namespace Editor {
|
||||
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
|
||||
@@ -12,7 +13,7 @@ export namespace Editor {
|
||||
const filepath = join(tmpdir(), `${Date.now()}.md`)
|
||||
await using _ = defer(async () => rm(filepath, { force: true }))
|
||||
|
||||
await Bun.write(filepath, opts.value)
|
||||
await Filesystem.write(filepath, opts.value)
|
||||
opts.renderer.suspend()
|
||||
opts.renderer.currentRenderBuffer.clear()
|
||||
const parts = editor.split(" ")
|
||||
@@ -23,7 +24,7 @@ export namespace Editor {
|
||||
stderr: "inherit",
|
||||
})
|
||||
await proc.exited
|
||||
const content = await Bun.file(filepath).text()
|
||||
const content = await Filesystem.readText(filepath)
|
||||
opts.renderer.currentRenderBuffer.clear()
|
||||
opts.renderer.resume()
|
||||
opts.renderer.requestRender()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
|
||||
interface UninstallArgs {
|
||||
keepConfig: boolean
|
||||
@@ -267,9 +268,7 @@ async function getShellConfigFile(): Promise<string | null> {
|
||||
.catch(() => false)
|
||||
if (!exists) continue
|
||||
|
||||
const content = await Bun.file(file)
|
||||
.text()
|
||||
.catch(() => "")
|
||||
const content = await Filesystem.readText(file).catch(() => "")
|
||||
if (content.includes("# opencode") || content.includes(".opencode/bin")) {
|
||||
return file
|
||||
}
|
||||
@@ -279,7 +278,7 @@ async function getShellConfigFile(): Promise<string | null> {
|
||||
}
|
||||
|
||||
async function cleanShellConfig(file: string) {
|
||||
const content = await Bun.file(file).text()
|
||||
const content = await Filesystem.readText(file)
|
||||
const lines = content.split("\n")
|
||||
|
||||
const filtered: string[] = []
|
||||
@@ -315,7 +314,7 @@ async function cleanShellConfig(file: string) {
|
||||
}
|
||||
|
||||
const output = filtered.join("\n") + "\n"
|
||||
await Bun.write(file, output)
|
||||
await Filesystem.write(file, output)
|
||||
}
|
||||
|
||||
async function getDirectorySize(dir: string): Promise<number> {
|
||||
|
||||
@@ -3,7 +3,6 @@ import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import os from "os"
|
||||
import z from "zod"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { ModelsDev } from "../provider/models"
|
||||
import { mergeDeep, pipe, unique } from "remeda"
|
||||
import { Global } from "../global"
|
||||
@@ -32,6 +31,8 @@ import { PackageRegistry } from "@/bun/registry"
|
||||
import { proxied } from "@/util/proxied"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Control } from "@/control"
|
||||
import { ConfigPaths } from "./paths"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
export namespace Config {
|
||||
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
||||
@@ -40,7 +41,7 @@ export namespace Config {
|
||||
|
||||
// Managed settings directory for enterprise deployments (highest priority, admin-controlled)
|
||||
// These settings override all user and project settings
|
||||
function getManagedConfigDir(): string {
|
||||
function systemManagedConfigDir(): string {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return "/Library/Application Support/opencode"
|
||||
@@ -51,10 +52,14 @@ export namespace Config {
|
||||
}
|
||||
}
|
||||
|
||||
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || getManagedConfigDir()
|
||||
export function managedConfigDir() {
|
||||
return process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir()
|
||||
}
|
||||
|
||||
const managedDir = managedConfigDir()
|
||||
|
||||
// Custom merge function that concatenates array fields instead of replacing them
|
||||
function merge(target: Info, source: Info): Info {
|
||||
function mergeConfigConcatArrays(target: Info, source: Info): Info {
|
||||
const merged = mergeDeep(target, source)
|
||||
if (target.plugin && source.plugin) {
|
||||
merged.plugin = Array.from(new Set([...target.plugin, ...source.plugin]))
|
||||
@@ -89,7 +94,10 @@ export namespace Config {
|
||||
const remoteConfig = wellknown.config ?? {}
|
||||
// Add $schema to prevent load() from trying to write back to a non-existent file
|
||||
if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
|
||||
result = merge(result, await load(JSON.stringify(remoteConfig), `${key}/.well-known/opencode`))
|
||||
result = mergeConfigConcatArrays(
|
||||
result,
|
||||
await load(JSON.stringify(remoteConfig), `${key}/.well-known/opencode`),
|
||||
)
|
||||
log.debug("loaded remote config from well-known", { url: key })
|
||||
}
|
||||
}
|
||||
@@ -99,21 +107,18 @@ export namespace Config {
|
||||
}
|
||||
|
||||
// Global user config overrides remote config.
|
||||
result = merge(result, await global())
|
||||
result = mergeConfigConcatArrays(result, await global())
|
||||
|
||||
// Custom config path overrides global config.
|
||||
if (Flag.OPENCODE_CONFIG) {
|
||||
result = merge(result, await loadFile(Flag.OPENCODE_CONFIG))
|
||||
result = mergeConfigConcatArrays(result, await loadFile(Flag.OPENCODE_CONFIG))
|
||||
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
|
||||
}
|
||||
|
||||
// Project config overrides global and remote config.
|
||||
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
|
||||
for (const resolved of found.toReversed()) {
|
||||
result = merge(result, await loadFile(resolved))
|
||||
}
|
||||
for (const file of await ConfigPaths.projectFiles("opencode", Instance.directory, Instance.worktree)) {
|
||||
result = mergeConfigConcatArrays(result, await loadFile(file))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,31 +126,10 @@ export namespace Config {
|
||||
result.mode = result.mode || {}
|
||||
result.plugin = result.plugin || []
|
||||
|
||||
const directories = [
|
||||
Global.Path.config,
|
||||
// Only scan project .opencode/ directories when project discovery is enabled
|
||||
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
|
||||
? await Array.fromAsync(
|
||||
Filesystem.up({
|
||||
targets: [".opencode"],
|
||||
start: Instance.directory,
|
||||
stop: Instance.worktree,
|
||||
}),
|
||||
)
|
||||
: []),
|
||||
// Always scan ~/.opencode/ (user home directory)
|
||||
...(await Array.fromAsync(
|
||||
Filesystem.up({
|
||||
targets: [".opencode"],
|
||||
start: Global.Path.home,
|
||||
stop: Global.Path.home,
|
||||
}),
|
||||
)),
|
||||
]
|
||||
const directories = await ConfigPaths.directories(Instance.directory, Instance.worktree)
|
||||
|
||||
// .opencode directory config overrides (project and global) config sources.
|
||||
if (Flag.OPENCODE_CONFIG_DIR) {
|
||||
directories.push(Flag.OPENCODE_CONFIG_DIR)
|
||||
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
|
||||
}
|
||||
|
||||
@@ -155,7 +139,7 @@ export namespace Config {
|
||||
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
log.debug(`loading config from ${path.join(dir, file)}`)
|
||||
result = merge(result, await loadFile(path.join(dir, file)))
|
||||
result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
|
||||
// to satisfy the type checker
|
||||
result.agent ??= {}
|
||||
result.mode ??= {}
|
||||
@@ -178,7 +162,7 @@ export namespace Config {
|
||||
|
||||
// Inline config content overrides all non-managed config sources.
|
||||
if (Flag.OPENCODE_CONFIG_CONTENT) {
|
||||
result = merge(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
|
||||
result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
|
||||
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
||||
}
|
||||
|
||||
@@ -186,9 +170,9 @@ export namespace Config {
|
||||
// Kept separate from directories array to avoid write operations when installing plugins
|
||||
// which would fail on system directories requiring elevated permissions
|
||||
// This way it only loads config file and not skills/plugins/commands
|
||||
if (existsSync(managedConfigDir)) {
|
||||
if (existsSync(managedDir)) {
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
result = merge(result, await loadFile(path.join(managedConfigDir, file)))
|
||||
result = mergeConfigConcatArrays(result, await loadFile(path.join(managedDir, file)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,8 +211,6 @@ export namespace Config {
|
||||
result.share = "auto"
|
||||
}
|
||||
|
||||
if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})
|
||||
|
||||
// Apply flag overrides for compaction settings
|
||||
if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) {
|
||||
result.compaction = { ...result.compaction, auto: false }
|
||||
@@ -291,7 +273,7 @@ export namespace Config {
|
||||
}
|
||||
}
|
||||
|
||||
async function needsInstall(dir: string) {
|
||||
export async function needsInstall(dir: string) {
|
||||
// Some config dirs may be read-only.
|
||||
// Installing deps there will fail; skip installation in that case.
|
||||
const writable = await isWritable(dir)
|
||||
@@ -918,20 +900,6 @@ export namespace Config {
|
||||
ref: "KeybindsConfig",
|
||||
})
|
||||
|
||||
export const TUI = z.object({
|
||||
scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"),
|
||||
scroll_acceleration: z
|
||||
.object({
|
||||
enabled: z.boolean().describe("Enable scroll acceleration"),
|
||||
})
|
||||
.optional()
|
||||
.describe("Scroll acceleration settings"),
|
||||
diff_style: z
|
||||
.enum(["auto", "stacked"])
|
||||
.optional()
|
||||
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
|
||||
})
|
||||
|
||||
export const Server = z
|
||||
.object({
|
||||
port: z.number().int().positive().optional().describe("Port to listen on"),
|
||||
@@ -1006,10 +974,7 @@ export namespace Config {
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||
logLevel: Log.Level.optional().describe("Log level"),
|
||||
tui: TUI.optional().describe("TUI specific settings"),
|
||||
server: Server.optional().describe("Server configuration for opencode serve and web commands"),
|
||||
command: z
|
||||
.record(z.string(), Command)
|
||||
@@ -1229,82 +1194,32 @@ export namespace Config {
|
||||
return result
|
||||
})
|
||||
|
||||
export const { readFile } = ConfigPaths
|
||||
|
||||
async function loadFile(filepath: string): Promise<Info> {
|
||||
log.info("loading", { path: filepath })
|
||||
let text = await Filesystem.readText(filepath).catch((err: any) => {
|
||||
if (err.code === "ENOENT") return
|
||||
throw new JsonError({ path: filepath }, { cause: err })
|
||||
})
|
||||
const text = await readFile(filepath)
|
||||
if (!text) return {}
|
||||
return load(text, filepath)
|
||||
}
|
||||
|
||||
async function load(text: string, configFilepath: string) {
|
||||
const original = text
|
||||
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||
return process.env[varName] || ""
|
||||
})
|
||||
const data = await ConfigPaths.parseText(text, configFilepath)
|
||||
|
||||
const fileMatches = text.match(/\{file:[^}]+\}/g)
|
||||
if (fileMatches) {
|
||||
const configDir = path.dirname(configFilepath)
|
||||
const lines = text.split("\n")
|
||||
const normalized = (() => {
|
||||
if (!data || typeof data !== "object" || Array.isArray(data)) return data
|
||||
const copy = { ...(data as Record<string, unknown>) }
|
||||
const hadLegacy = "theme" in copy || "keybinds" in copy || "tui" in copy
|
||||
if (!hadLegacy) return copy
|
||||
delete copy.theme
|
||||
delete copy.keybinds
|
||||
delete copy.tui
|
||||
log.warn("tui keys in opencode config are deprecated; move them to tui.json", { path: configFilepath })
|
||||
return copy
|
||||
})()
|
||||
|
||||
for (const match of fileMatches) {
|
||||
const lineIndex = lines.findIndex((line) => line.includes(match))
|
||||
if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) {
|
||||
continue // Skip if line is commented
|
||||
}
|
||||
let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
|
||||
if (filePath.startsWith("~/")) {
|
||||
filePath = path.join(os.homedir(), filePath.slice(2))
|
||||
}
|
||||
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
||||
const fileContent = (
|
||||
await Filesystem.readText(resolvedPath).catch((error: any) => {
|
||||
const errMsg = `bad file reference: "${match}"`
|
||||
if (error.code === "ENOENT") {
|
||||
throw new InvalidError(
|
||||
{
|
||||
path: configFilepath,
|
||||
message: errMsg + ` ${resolvedPath} does not exist`,
|
||||
},
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
throw new InvalidError({ path: configFilepath, message: errMsg }, { cause: error })
|
||||
})
|
||||
).trim()
|
||||
// escape newlines/quotes, strip outer quotes
|
||||
text = text.replace(match, () => JSON.stringify(fileContent).slice(1, -1))
|
||||
}
|
||||
}
|
||||
|
||||
const errors: JsoncParseError[] = []
|
||||
const data = parseJsonc(text, errors, { allowTrailingComma: true })
|
||||
if (errors.length) {
|
||||
const lines = text.split("\n")
|
||||
const errorDetails = errors
|
||||
.map((e) => {
|
||||
const beforeOffset = text.substring(0, e.offset).split("\n")
|
||||
const line = beforeOffset.length
|
||||
const column = beforeOffset[beforeOffset.length - 1].length + 1
|
||||
const problemLine = lines[line - 1]
|
||||
|
||||
const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
|
||||
if (!problemLine) return error
|
||||
|
||||
return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
throw new JsonError({
|
||||
path: configFilepath,
|
||||
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
|
||||
})
|
||||
}
|
||||
|
||||
const parsed = Info.safeParse(data)
|
||||
const parsed = Info.safeParse(normalized)
|
||||
if (parsed.success) {
|
||||
if (!parsed.data.$schema) {
|
||||
parsed.data.$schema = "https://opencode.ai/config.json"
|
||||
@@ -1329,13 +1244,7 @@ export namespace Config {
|
||||
issues: parsed.error.issues,
|
||||
})
|
||||
}
|
||||
export const JsonError = NamedError.create(
|
||||
"ConfigJsonError",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
export const { JsonError, InvalidError } = ConfigPaths
|
||||
|
||||
export const ConfigDirectoryTypoError = NamedError.create(
|
||||
"ConfigDirectoryTypoError",
|
||||
@@ -1346,15 +1255,6 @@ export namespace Config {
|
||||
}),
|
||||
)
|
||||
|
||||
export const InvalidError = NamedError.create(
|
||||
"ConfigInvalidError",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
issues: z.custom<z.core.$ZodIssue[]>().optional(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
export async function get() {
|
||||
return state().then((x) => x.config)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import matter from "gray-matter"
|
||||
import { z } from "zod"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
export namespace ConfigMarkdown {
|
||||
export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
|
||||
@@ -68,7 +69,7 @@ export namespace ConfigMarkdown {
|
||||
}
|
||||
|
||||
export async function parse(filePath: string) {
|
||||
const template = await Bun.file(filePath).text()
|
||||
const template = await Filesystem.readText(filePath)
|
||||
|
||||
try {
|
||||
const md = matter(template)
|
||||
|
||||
155
packages/opencode/src/config/migrate-tui-config.ts
Normal file
155
packages/opencode/src/config/migrate-tui-config.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import path from "path"
|
||||
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser"
|
||||
import { unique } from "remeda"
|
||||
import z from "zod"
|
||||
import { ConfigPaths } from "./paths"
|
||||
import { TuiInfo, TuiOptions } from "./tui-schema"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Log } from "@/util/log"
|
||||
import { Global } from "@/global"
|
||||
|
||||
const log = Log.create({ service: "tui.migrate" })
|
||||
|
||||
const TUI_SCHEMA_URL = "https://opencode.ai/tui.json"
|
||||
|
||||
const LegacyTheme = TuiInfo.shape.theme.optional()
|
||||
const LegacyRecord = z.record(z.string(), z.unknown()).optional()
|
||||
|
||||
const TuiLegacy = z
|
||||
.object({
|
||||
scroll_speed: TuiOptions.shape.scroll_speed.catch(undefined),
|
||||
scroll_acceleration: TuiOptions.shape.scroll_acceleration.catch(undefined),
|
||||
diff_style: TuiOptions.shape.diff_style.catch(undefined),
|
||||
})
|
||||
.strip()
|
||||
|
||||
interface MigrateInput {
|
||||
directories: string[]
|
||||
custom?: string
|
||||
managed: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates tui-specific keys (theme, keybinds, tui) from opencode.json files
|
||||
* into dedicated tui.json files. Migration is performed per-directory and
|
||||
* skips only locations where a tui.json already exists.
|
||||
*/
|
||||
export async function migrateTuiConfig(input: MigrateInput) {
|
||||
const opencode = await opencodeFiles(input)
|
||||
for (const file of opencode) {
|
||||
const source = await Bun.file(file)
|
||||
.text()
|
||||
.catch((error) => {
|
||||
log.warn("failed to read config for tui migration", { path: file, error })
|
||||
return undefined
|
||||
})
|
||||
if (!source) continue
|
||||
const data = parseJsonc(source)
|
||||
if (!data || typeof data !== "object" || Array.isArray(data)) continue
|
||||
|
||||
const theme = LegacyTheme.safeParse("theme" in data ? data.theme : undefined)
|
||||
const keybinds = LegacyRecord.safeParse("keybinds" in data ? data.keybinds : undefined)
|
||||
const legacyTui = LegacyRecord.safeParse("tui" in data ? data.tui : undefined)
|
||||
const extracted = {
|
||||
theme: theme.success ? theme.data : undefined,
|
||||
keybinds: keybinds.success ? keybinds.data : undefined,
|
||||
tui: legacyTui.success ? legacyTui.data : undefined,
|
||||
}
|
||||
const tui = extracted.tui ? normalizeTui(extracted.tui) : undefined
|
||||
if (extracted.theme === undefined && extracted.keybinds === undefined && !tui) continue
|
||||
|
||||
const target = path.join(path.dirname(file), "tui.json")
|
||||
const targetExists = await Bun.file(target).exists()
|
||||
if (targetExists) continue
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
$schema: TUI_SCHEMA_URL,
|
||||
}
|
||||
if (extracted.theme !== undefined) payload.theme = extracted.theme
|
||||
if (extracted.keybinds !== undefined) payload.keybinds = extracted.keybinds
|
||||
if (tui) Object.assign(payload, tui)
|
||||
|
||||
const wrote = await Bun.write(target, JSON.stringify(payload, null, 2))
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.warn("failed to write tui migration target", { from: file, to: target, error })
|
||||
return false
|
||||
})
|
||||
if (!wrote) continue
|
||||
|
||||
const stripped = await backupAndStripLegacy(file, source)
|
||||
if (!stripped) {
|
||||
log.warn("tui config migrated but source file was not stripped", { from: file, to: target })
|
||||
continue
|
||||
}
|
||||
log.info("migrated tui config", { from: file, to: target })
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTui(data: Record<string, unknown>) {
|
||||
const parsed = TuiLegacy.parse(data)
|
||||
if (
|
||||
parsed.scroll_speed === undefined &&
|
||||
parsed.diff_style === undefined &&
|
||||
parsed.scroll_acceleration === undefined
|
||||
) {
|
||||
return
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
async function backupAndStripLegacy(file: string, source: string) {
|
||||
const backup = file + ".tui-migration.bak"
|
||||
const hasBackup = await Bun.file(backup).exists()
|
||||
const backed = hasBackup
|
||||
? true
|
||||
: await Bun.write(backup, source)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.warn("failed to backup source config during tui migration", { path: file, backup, error })
|
||||
return false
|
||||
})
|
||||
if (!backed) return false
|
||||
|
||||
const text = ["theme", "keybinds", "tui"].reduce((acc, key) => {
|
||||
const edits = modify(acc, [key], undefined, {
|
||||
formattingOptions: {
|
||||
insertSpaces: true,
|
||||
tabSize: 2,
|
||||
},
|
||||
})
|
||||
if (!edits.length) return acc
|
||||
return applyEdits(acc, edits)
|
||||
}, source)
|
||||
|
||||
return Bun.write(file, text)
|
||||
.then(() => {
|
||||
log.info("stripped tui keys from server config", { path: file, backup })
|
||||
return true
|
||||
})
|
||||
.catch((error) => {
|
||||
log.warn("failed to strip legacy tui keys from server config", { path: file, backup, error })
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
async function opencodeFiles(input: { directories: string[]; managed: string }) {
|
||||
const project = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
|
||||
? []
|
||||
: await ConfigPaths.projectFiles("opencode", Instance.directory, Instance.worktree)
|
||||
const files = [...project, ...ConfigPaths.fileInDirectory(Global.Path.config, "opencode")]
|
||||
for (const dir of unique(input.directories)) {
|
||||
files.push(...ConfigPaths.fileInDirectory(dir, "opencode"))
|
||||
}
|
||||
if (Flag.OPENCODE_CONFIG) files.push(Flag.OPENCODE_CONFIG)
|
||||
files.push(...ConfigPaths.fileInDirectory(input.managed, "opencode"))
|
||||
|
||||
const existing = await Promise.all(
|
||||
unique(files).map(async (file) => {
|
||||
const ok = await Bun.file(file).exists()
|
||||
return ok ? file : undefined
|
||||
}),
|
||||
)
|
||||
return existing.filter((file): file is string => !!file)
|
||||
}
|
||||
154
packages/opencode/src/config/paths.ts
Normal file
154
packages/opencode/src/config/paths.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import z from "zod"
|
||||
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Global } from "@/global"
|
||||
|
||||
export namespace ConfigPaths {
|
||||
export async function projectFiles(name: string, directory: string, worktree: string) {
|
||||
const files: string[] = []
|
||||
for (const file of [`${name}.jsonc`, `${name}.json`]) {
|
||||
const found = await Filesystem.findUp(file, directory, worktree)
|
||||
for (const resolved of found.toReversed()) {
|
||||
files.push(resolved)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
export async function directories(directory: string, worktree: string) {
|
||||
return [
|
||||
Global.Path.config,
|
||||
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
|
||||
? await Array.fromAsync(
|
||||
Filesystem.up({
|
||||
targets: [".opencode"],
|
||||
start: directory,
|
||||
stop: worktree,
|
||||
}),
|
||||
)
|
||||
: []),
|
||||
...(await Array.fromAsync(
|
||||
Filesystem.up({
|
||||
targets: [".opencode"],
|
||||
start: Global.Path.home,
|
||||
stop: Global.Path.home,
|
||||
}),
|
||||
)),
|
||||
...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []),
|
||||
]
|
||||
}
|
||||
|
||||
export function fileInDirectory(dir: string, name: string) {
|
||||
return [path.join(dir, `${name}.jsonc`), path.join(dir, `${name}.json`)]
|
||||
}
|
||||
|
||||
export const JsonError = NamedError.create(
|
||||
"ConfigJsonError",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const InvalidError = NamedError.create(
|
||||
"ConfigInvalidError",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
issues: z.custom<z.core.$ZodIssue[]>().optional(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
/** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */
|
||||
export async function readFile(filepath: string) {
|
||||
return Bun.file(filepath)
|
||||
.text()
|
||||
.catch((err) => {
|
||||
if (err.code === "ENOENT") return
|
||||
throw new JsonError({ path: filepath }, { cause: err })
|
||||
})
|
||||
}
|
||||
|
||||
/** Apply {env:VAR} and {file:path} substitutions to config text. */
|
||||
async function substitute(text: string, configFilepath: string, missing: "error" | "empty" = "error") {
|
||||
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||
return process.env[varName] || ""
|
||||
})
|
||||
|
||||
const fileMatches = text.match(/\{file:[^}]+\}/g)
|
||||
if (!fileMatches) return text
|
||||
|
||||
const configDir = path.dirname(configFilepath)
|
||||
const lines = text.split("\n")
|
||||
|
||||
for (const match of fileMatches) {
|
||||
const lineIndex = lines.findIndex((line) => line.includes(match))
|
||||
if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) continue
|
||||
|
||||
let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
|
||||
if (filePath.startsWith("~/")) {
|
||||
filePath = path.join(os.homedir(), filePath.slice(2))
|
||||
}
|
||||
|
||||
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
||||
const fileContent = (
|
||||
await Bun.file(resolvedPath)
|
||||
.text()
|
||||
.catch((error) => {
|
||||
if (missing === "empty") return ""
|
||||
|
||||
const errMsg = `bad file reference: "${match}"`
|
||||
if (error.code === "ENOENT") {
|
||||
throw new InvalidError(
|
||||
{
|
||||
path: configFilepath,
|
||||
message: errMsg + ` ${resolvedPath} does not exist`,
|
||||
},
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
throw new InvalidError({ path: configFilepath, message: errMsg }, { cause: error })
|
||||
})
|
||||
).trim()
|
||||
|
||||
text = text.replace(match, () => JSON.stringify(fileContent).slice(1, -1))
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
/** Substitute and parse JSONC text, throwing JsonError on syntax errors. */
|
||||
export async function parseText(text: string, configFilepath: string, missing: "error" | "empty" = "error") {
|
||||
text = await substitute(text, configFilepath, missing)
|
||||
|
||||
const errors: JsoncParseError[] = []
|
||||
const data = parseJsonc(text, errors, { allowTrailingComma: true })
|
||||
if (errors.length) {
|
||||
const lines = text.split("\n")
|
||||
const errorDetails = errors
|
||||
.map((e) => {
|
||||
const beforeOffset = text.substring(0, e.offset).split("\n")
|
||||
const line = beforeOffset.length
|
||||
const column = beforeOffset[beforeOffset.length - 1].length + 1
|
||||
const problemLine = lines[line - 1]
|
||||
|
||||
const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
|
||||
if (!problemLine) return error
|
||||
|
||||
return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
throw new JsonError({
|
||||
path: configFilepath,
|
||||
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
25
packages/opencode/src/config/tui-schema.ts
Normal file
25
packages/opencode/src/config/tui-schema.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import z from "zod"
|
||||
import { Config } from "./config"
|
||||
|
||||
export const TuiOptions = z.object({
|
||||
scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"),
|
||||
scroll_acceleration: z
|
||||
.object({
|
||||
enabled: z.boolean().describe("Enable scroll acceleration"),
|
||||
})
|
||||
.optional()
|
||||
.describe("Scroll acceleration settings"),
|
||||
diff_style: z
|
||||
.enum(["auto", "stacked"])
|
||||
.optional()
|
||||
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
|
||||
})
|
||||
|
||||
export const TuiInfo = z
|
||||
.object({
|
||||
$schema: z.string().optional(),
|
||||
theme: z.string().optional(),
|
||||
keybinds: Config.Keybinds.optional(),
|
||||
})
|
||||
.extend(TuiOptions.shape)
|
||||
.strict()
|
||||
118
packages/opencode/src/config/tui.ts
Normal file
118
packages/opencode/src/config/tui.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { existsSync } from "fs"
|
||||
import z from "zod"
|
||||
import { mergeDeep, unique } from "remeda"
|
||||
import { Config } from "./config"
|
||||
import { ConfigPaths } from "./paths"
|
||||
import { migrateTuiConfig } from "./migrate-tui-config"
|
||||
import { TuiInfo } from "./tui-schema"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Log } from "@/util/log"
|
||||
import { Global } from "@/global"
|
||||
|
||||
export namespace TuiConfig {
|
||||
const log = Log.create({ service: "tui.config" })
|
||||
|
||||
export const Info = TuiInfo
|
||||
|
||||
export type Info = z.output<typeof Info>
|
||||
|
||||
function mergeInfo(target: Info, source: Info): Info {
|
||||
return mergeDeep(target, source)
|
||||
}
|
||||
|
||||
function customPath() {
|
||||
return Flag.OPENCODE_TUI_CONFIG
|
||||
}
|
||||
|
||||
const state = Instance.state(async () => {
|
||||
let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
|
||||
? []
|
||||
: await ConfigPaths.projectFiles("tui", Instance.directory, Instance.worktree)
|
||||
const directories = await ConfigPaths.directories(Instance.directory, Instance.worktree)
|
||||
const custom = customPath()
|
||||
const managed = Config.managedConfigDir()
|
||||
await migrateTuiConfig({ directories, custom, managed })
|
||||
// Re-compute after migration since migrateTuiConfig may have created new tui.json files
|
||||
projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
|
||||
? []
|
||||
: await ConfigPaths.projectFiles("tui", Instance.directory, Instance.worktree)
|
||||
|
||||
let result: Info = {}
|
||||
|
||||
for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
|
||||
result = mergeInfo(result, await loadFile(file))
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
result = mergeInfo(result, await loadFile(custom))
|
||||
log.debug("loaded custom tui config", { path: custom })
|
||||
}
|
||||
|
||||
for (const file of projectFiles) {
|
||||
result = mergeInfo(result, await loadFile(file))
|
||||
}
|
||||
|
||||
for (const dir of unique(directories)) {
|
||||
if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
|
||||
for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
|
||||
result = mergeInfo(result, await loadFile(file))
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(managed)) {
|
||||
for (const file of ConfigPaths.fileInDirectory(managed, "tui")) {
|
||||
result = mergeInfo(result, await loadFile(file))
|
||||
}
|
||||
}
|
||||
|
||||
result.keybinds ??= Config.Keybinds.parse({})
|
||||
|
||||
return {
|
||||
config: result,
|
||||
}
|
||||
})
|
||||
|
||||
export async function get() {
|
||||
return state().then((x) => x.config)
|
||||
}
|
||||
|
||||
async function loadFile(filepath: string): Promise<Info> {
|
||||
const text = await ConfigPaths.readFile(filepath)
|
||||
if (!text) return {}
|
||||
return load(text, filepath).catch((error) => {
|
||||
log.warn("failed to load tui config", { path: filepath, error })
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
async function load(text: string, configFilepath: string): Promise<Info> {
|
||||
const data = await ConfigPaths.parseText(text, configFilepath, "empty")
|
||||
if (!data || typeof data !== "object" || Array.isArray(data)) return {}
|
||||
|
||||
// Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
|
||||
// (mirroring the old opencode.json shape) still get their settings applied.
|
||||
const normalized = (() => {
|
||||
const copy = { ...(data as Record<string, unknown>) }
|
||||
if (!("tui" in copy)) return copy
|
||||
if (!copy.tui || typeof copy.tui !== "object" || Array.isArray(copy.tui)) {
|
||||
delete copy.tui
|
||||
return copy
|
||||
}
|
||||
const tui = copy.tui as Record<string, unknown>
|
||||
delete copy.tui
|
||||
return {
|
||||
...tui,
|
||||
...copy,
|
||||
}
|
||||
})()
|
||||
|
||||
const parsed = Info.safeParse(normalized)
|
||||
if (!parsed.success) {
|
||||
log.warn("invalid tui config", { path: configFilepath, issues: parsed.error.issues })
|
||||
return {}
|
||||
}
|
||||
|
||||
return parsed.data
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import z from "zod"
|
||||
import { $ } from "bun"
|
||||
import type { BunFile } from "bun"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
@@ -241,8 +240,8 @@ export namespace File {
|
||||
return mimeType.startsWith("image/")
|
||||
}
|
||||
|
||||
async function shouldEncode(file: BunFile): Promise<boolean> {
|
||||
const type = file.type?.toLowerCase()
|
||||
async function shouldEncode(mimeType: string): Promise<boolean> {
|
||||
const type = mimeType.toLowerCase()
|
||||
log.info("shouldEncode", { type })
|
||||
if (!type) return false
|
||||
|
||||
@@ -385,7 +384,7 @@ export namespace File {
|
||||
const untrackedFiles = untrackedOutput.trim().split("\n")
|
||||
for (const filepath of untrackedFiles) {
|
||||
try {
|
||||
const content = await Bun.file(path.join(Instance.directory, filepath)).text()
|
||||
const content = await Filesystem.readText(path.join(Instance.directory, filepath))
|
||||
const lines = content.split("\n").length
|
||||
changedFiles.push({
|
||||
path: filepath,
|
||||
@@ -437,10 +436,9 @@ export namespace File {
|
||||
|
||||
// Fast path: check extension before any filesystem operations
|
||||
if (isImageByExtension(file)) {
|
||||
const bunFile = Bun.file(full)
|
||||
if (await bunFile.exists()) {
|
||||
const buffer = await bunFile.arrayBuffer().catch(() => new ArrayBuffer(0))
|
||||
const content = Buffer.from(buffer).toString("base64")
|
||||
if (await Filesystem.exists(full)) {
|
||||
const buffer = await Filesystem.readBytes(full).catch(() => Buffer.from([]))
|
||||
const content = buffer.toString("base64")
|
||||
const mimeType = getImageMimeType(file)
|
||||
return { type: "text", content, mimeType, encoding: "base64" }
|
||||
}
|
||||
@@ -451,29 +449,24 @@ export namespace File {
|
||||
return { type: "binary", content: "" }
|
||||
}
|
||||
|
||||
const bunFile = Bun.file(full)
|
||||
|
||||
if (!(await bunFile.exists())) {
|
||||
if (!(await Filesystem.exists(full))) {
|
||||
return { type: "text", content: "" }
|
||||
}
|
||||
|
||||
const encode = await shouldEncode(bunFile)
|
||||
const mimeType = bunFile.type || "application/octet-stream"
|
||||
const mimeType = Filesystem.mimeType(full)
|
||||
const encode = await shouldEncode(mimeType)
|
||||
|
||||
if (encode && !isImage(mimeType)) {
|
||||
return { type: "binary", content: "", mimeType }
|
||||
}
|
||||
|
||||
if (encode) {
|
||||
const buffer = await bunFile.arrayBuffer().catch(() => new ArrayBuffer(0))
|
||||
const content = Buffer.from(buffer).toString("base64")
|
||||
const buffer = await Filesystem.readBytes(full).catch(() => Buffer.from([]))
|
||||
const content = buffer.toString("base64")
|
||||
return { type: "text", content, mimeType, encoding: "base64" }
|
||||
}
|
||||
|
||||
const content = await bunFile
|
||||
.text()
|
||||
.catch(() => "")
|
||||
.then((x) => x.trim())
|
||||
const content = (await Filesystem.readText(full).catch(() => "")).trim()
|
||||
|
||||
if (project.vcs === "git") {
|
||||
let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
@@ -497,13 +490,13 @@ export namespace File {
|
||||
let ignored = (_: string) => false
|
||||
if (project.vcs === "git") {
|
||||
const ig = ignore()
|
||||
const gitignore = Bun.file(path.join(Instance.worktree, ".gitignore"))
|
||||
if (await gitignore.exists()) {
|
||||
ig.add(await gitignore.text())
|
||||
const gitignorePath = path.join(Instance.worktree, ".gitignore")
|
||||
if (await Filesystem.exists(gitignorePath)) {
|
||||
ig.add(await Filesystem.readText(gitignorePath))
|
||||
}
|
||||
const ignoreFile = Bun.file(path.join(Instance.worktree, ".ignore"))
|
||||
if (await ignoreFile.exists()) {
|
||||
ig.add(await ignoreFile.text())
|
||||
const ignorePath = path.join(Instance.worktree, ".ignore")
|
||||
if (await Filesystem.exists(ignorePath)) {
|
||||
ig.add(await Filesystem.readText(ignorePath))
|
||||
}
|
||||
ignored = ig.ignores.bind(ig)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Instance } from "../project/instance"
|
||||
import { Log } from "../util/log"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
export namespace FileTime {
|
||||
const log = Log.create({ service: "file.time" })
|
||||
@@ -59,10 +60,10 @@ export namespace FileTime {
|
||||
|
||||
const time = get(sessionID, filepath)
|
||||
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
|
||||
const stats = await Bun.file(filepath).stat()
|
||||
if (stats.mtime.getTime() > time.getTime()) {
|
||||
const mtime = Filesystem.stat(filepath)?.mtime
|
||||
if (mtime && mtime.getTime() > time.getTime()) {
|
||||
throw new Error(
|
||||
`File ${filepath} has been modified since it was last read.\nLast modification: ${stats.mtime.toISOString()}\nLast read: ${time.toISOString()}\n\nPlease read the file again before modifying it.`,
|
||||
`File ${filepath} has been modified since it was last read.\nLast modification: ${mtime.toISOString()}\nLast read: ${time.toISOString()}\n\nPlease read the file again before modifying it.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export namespace Flag {
|
||||
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
|
||||
export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"]
|
||||
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
|
||||
export declare const OPENCODE_TUI_CONFIG: string | undefined
|
||||
export declare const OPENCODE_CONFIG_DIR: string | undefined
|
||||
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
|
||||
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
|
||||
@@ -74,6 +75,17 @@ Object.defineProperty(Flag, "OPENCODE_DISABLE_PROJECT_CONFIG", {
|
||||
configurable: false,
|
||||
})
|
||||
|
||||
// Dynamic getter for OPENCODE_TUI_CONFIG
|
||||
// This must be evaluated at access time, not module load time,
|
||||
// because tests and external tooling may set this env var at runtime
|
||||
Object.defineProperty(Flag, "OPENCODE_TUI_CONFIG", {
|
||||
get() {
|
||||
return process.env["OPENCODE_TUI_CONFIG"]
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
})
|
||||
|
||||
// Dynamic getter for OPENCODE_CONFIG_DIR
|
||||
// This must be evaluated at access time, not module load time,
|
||||
// because external tooling may set this env var at runtime
|
||||
|
||||
@@ -67,7 +67,10 @@ export const prettier: Info = {
|
||||
async enabled() {
|
||||
const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
|
||||
for (const item of items) {
|
||||
const json = await Bun.file(item).json()
|
||||
const json = await Filesystem.readJson<{
|
||||
dependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
}>(item)
|
||||
if (json.dependencies?.prettier) return true
|
||||
if (json.devDependencies?.prettier) return true
|
||||
}
|
||||
@@ -86,7 +89,10 @@ export const oxfmt: Info = {
|
||||
if (!Flag.OPENCODE_EXPERIMENTAL_OXFMT) return false
|
||||
const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
|
||||
for (const item of items) {
|
||||
const json = await Bun.file(item).json()
|
||||
const json = await Filesystem.readJson<{
|
||||
dependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
}>(item)
|
||||
if (json.dependencies?.oxfmt) return true
|
||||
if (json.devDependencies?.oxfmt) return true
|
||||
}
|
||||
@@ -179,7 +185,7 @@ export const ruff: Info = {
|
||||
const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
|
||||
if (found.length > 0) {
|
||||
if (config === "pyproject.toml") {
|
||||
const content = await Bun.file(found[0]).text()
|
||||
const content = await Filesystem.readText(found[0])
|
||||
if (content.includes("[tool.ruff]")) return true
|
||||
} else {
|
||||
return true
|
||||
@@ -190,7 +196,7 @@ export const ruff: Info = {
|
||||
for (const dep of deps) {
|
||||
const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
|
||||
if (found.length > 0) {
|
||||
const content = await Bun.file(found[0]).text()
|
||||
const content = await Filesystem.readText(found[0])
|
||||
if (content.includes("ruff")) return true
|
||||
}
|
||||
}
|
||||
@@ -348,7 +354,10 @@ export const pint: Info = {
|
||||
async enabled() {
|
||||
const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree)
|
||||
for (const item of items) {
|
||||
const json = await Bun.file(item).json()
|
||||
const json = await Filesystem.readJson<{
|
||||
require?: Record<string, string>
|
||||
"require-dev"?: Record<string, string>
|
||||
}>(item)
|
||||
if (json.require?.["laravel/pint"]) return true
|
||||
if (json["require-dev"]?.["laravel/pint"]) return true
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "fs/promises"
|
||||
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
const app = "opencode"
|
||||
|
||||
@@ -35,9 +36,7 @@ await Promise.all([
|
||||
|
||||
const CACHE_VERSION = "21"
|
||||
|
||||
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
||||
.text()
|
||||
.catch(() => "0")
|
||||
const version = await Filesystem.readText(path.join(Global.Path.cache, "version")).catch(() => "0")
|
||||
|
||||
if (version !== CACHE_VERSION) {
|
||||
try {
|
||||
@@ -51,5 +50,5 @@ if (version !== CACHE_VERSION) {
|
||||
),
|
||||
)
|
||||
} catch (e) {}
|
||||
await Bun.file(path.join(Global.Path.cache, "version")).write(CACHE_VERSION)
|
||||
await Filesystem.write(path.join(Global.Path.cache, "version"), CACHE_VERSION)
|
||||
}
|
||||
|
||||
@@ -147,8 +147,7 @@ export namespace LSPClient {
|
||||
notify: {
|
||||
async open(input: { path: string }) {
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
|
||||
const file = Bun.file(input.path)
|
||||
const text = await file.text()
|
||||
const text = await Filesystem.readText(input.path)
|
||||
const extension = path.extname(input.path)
|
||||
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ export namespace LSPServer {
|
||||
"bin",
|
||||
"vue-language-server.js",
|
||||
)
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
@@ -173,14 +173,14 @@ export namespace LSPServer {
|
||||
if (!eslint) return
|
||||
log.info("spawning eslint server")
|
||||
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
|
||||
if (!(await Bun.file(serverPath).exists())) {
|
||||
if (!(await Filesystem.exists(serverPath))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading and building VS Code ESLint server")
|
||||
const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
|
||||
if (!response.ok) return
|
||||
|
||||
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
|
||||
await Bun.file(zipPath).write(response)
|
||||
if (response.body) await Filesystem.writeStream(zipPath, response.body)
|
||||
|
||||
const ok = await Archive.extractZip(zipPath, Global.Path.bin)
|
||||
.then(() => true)
|
||||
@@ -242,7 +242,7 @@ export namespace LSPServer {
|
||||
|
||||
const resolveBin = async (target: string) => {
|
||||
const localBin = path.join(root, target)
|
||||
if (await Bun.file(localBin).exists()) return localBin
|
||||
if (await Filesystem.exists(localBin)) return localBin
|
||||
|
||||
const candidates = Filesystem.up({
|
||||
targets: [target],
|
||||
@@ -326,7 +326,7 @@ export namespace LSPServer {
|
||||
async spawn(root) {
|
||||
const localBin = path.join(root, "node_modules", ".bin", "biome")
|
||||
let bin: string | undefined
|
||||
if (await Bun.file(localBin).exists()) bin = localBin
|
||||
if (await Filesystem.exists(localBin)) bin = localBin
|
||||
if (!bin) {
|
||||
const found = Bun.which("biome")
|
||||
if (found) bin = found
|
||||
@@ -467,7 +467,7 @@ export namespace LSPServer {
|
||||
const potentialPythonPath = isWindows
|
||||
? path.join(venvPath, "Scripts", "python.exe")
|
||||
: path.join(venvPath, "bin", "python")
|
||||
if (await Bun.file(potentialPythonPath).exists()) {
|
||||
if (await Filesystem.exists(potentialPythonPath)) {
|
||||
initialization["pythonPath"] = potentialPythonPath
|
||||
break
|
||||
}
|
||||
@@ -479,7 +479,7 @@ export namespace LSPServer {
|
||||
const potentialTyPath = isWindows
|
||||
? path.join(venvPath, "Scripts", "ty.exe")
|
||||
: path.join(venvPath, "bin", "ty")
|
||||
if (await Bun.file(potentialTyPath).exists()) {
|
||||
if (await Filesystem.exists(potentialTyPath)) {
|
||||
binary = potentialTyPath
|
||||
break
|
||||
}
|
||||
@@ -511,7 +511,7 @@ export namespace LSPServer {
|
||||
const args = []
|
||||
if (!binary) {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "pyright"], {
|
||||
cwd: Global.Path.bin,
|
||||
@@ -536,7 +536,7 @@ export namespace LSPServer {
|
||||
const potentialPythonPath = isWindows
|
||||
? path.join(venvPath, "Scripts", "python.exe")
|
||||
: path.join(venvPath, "bin", "python")
|
||||
if (await Bun.file(potentialPythonPath).exists()) {
|
||||
if (await Filesystem.exists(potentialPythonPath)) {
|
||||
initialization["pythonPath"] = potentialPythonPath
|
||||
break
|
||||
}
|
||||
@@ -571,7 +571,7 @@ export namespace LSPServer {
|
||||
process.platform === "win32" ? "language_server.bat" : "language_server.sh",
|
||||
)
|
||||
|
||||
if (!(await Bun.file(binary).exists())) {
|
||||
if (!(await Filesystem.exists(binary))) {
|
||||
const elixir = Bun.which("elixir")
|
||||
if (!elixir) {
|
||||
log.error("elixir is required to run elixir-ls")
|
||||
@@ -584,7 +584,7 @@ export namespace LSPServer {
|
||||
const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
|
||||
if (!response.ok) return
|
||||
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
|
||||
await Bun.file(zipPath).write(response)
|
||||
if (response.body) await Filesystem.writeStream(zipPath, response.body)
|
||||
|
||||
const ok = await Archive.extractZip(zipPath, Global.Path.bin)
|
||||
.then(() => true)
|
||||
@@ -692,7 +692,7 @@ export namespace LSPServer {
|
||||
}
|
||||
|
||||
const tempPath = path.join(Global.Path.bin, assetName)
|
||||
await Bun.file(tempPath).write(downloadResponse)
|
||||
if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
|
||||
|
||||
if (ext === "zip") {
|
||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
||||
@@ -710,7 +710,7 @@ export namespace LSPServer {
|
||||
|
||||
bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Bun.file(bin).exists())) {
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract zls binary")
|
||||
return
|
||||
}
|
||||
@@ -857,7 +857,7 @@ export namespace LSPServer {
|
||||
// Stop at filesystem root
|
||||
const cargoTomlPath = path.join(currentDir, "Cargo.toml")
|
||||
try {
|
||||
const cargoTomlContent = await Bun.file(cargoTomlPath).text()
|
||||
const cargoTomlContent = await Filesystem.readText(cargoTomlPath)
|
||||
if (cargoTomlContent.includes("[workspace]")) {
|
||||
return currentDir
|
||||
}
|
||||
@@ -907,7 +907,7 @@ export namespace LSPServer {
|
||||
|
||||
const ext = process.platform === "win32" ? ".exe" : ""
|
||||
const direct = path.join(Global.Path.bin, "clangd" + ext)
|
||||
if (await Bun.file(direct).exists()) {
|
||||
if (await Filesystem.exists(direct)) {
|
||||
return {
|
||||
process: spawn(direct, args, {
|
||||
cwd: root,
|
||||
@@ -920,7 +920,7 @@ export namespace LSPServer {
|
||||
if (!entry.isDirectory()) continue
|
||||
if (!entry.name.startsWith("clangd_")) continue
|
||||
const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
|
||||
if (await Bun.file(candidate).exists()) {
|
||||
if (await Filesystem.exists(candidate)) {
|
||||
return {
|
||||
process: spawn(candidate, args, {
|
||||
cwd: root,
|
||||
@@ -990,7 +990,7 @@ export namespace LSPServer {
|
||||
log.error("Failed to write clangd archive")
|
||||
return
|
||||
}
|
||||
await Bun.write(archive, buf)
|
||||
await Filesystem.write(archive, Buffer.from(buf))
|
||||
|
||||
const zip = name.endsWith(".zip")
|
||||
const tar = name.endsWith(".tar.xz")
|
||||
@@ -1014,7 +1014,7 @@ export namespace LSPServer {
|
||||
await fs.rm(archive, { force: true })
|
||||
|
||||
const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
|
||||
if (!(await Bun.file(bin).exists())) {
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract clangd binary")
|
||||
return
|
||||
}
|
||||
@@ -1045,7 +1045,7 @@ export namespace LSPServer {
|
||||
const args: string[] = []
|
||||
if (!binary) {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
@@ -1092,7 +1092,7 @@ export namespace LSPServer {
|
||||
const args: string[] = []
|
||||
if (!binary) {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
@@ -1248,7 +1248,7 @@ export namespace LSPServer {
|
||||
const distPath = path.join(Global.Path.bin, "kotlin-ls")
|
||||
const launcherScript =
|
||||
process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh")
|
||||
const installed = await Bun.file(launcherScript).exists()
|
||||
const installed = await Filesystem.exists(launcherScript)
|
||||
if (!installed) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("Downloading Kotlin Language Server from GitHub.")
|
||||
@@ -1307,7 +1307,7 @@ export namespace LSPServer {
|
||||
}
|
||||
log.info("Installed Kotlin Language Server", { path: launcherScript })
|
||||
}
|
||||
if (!(await Bun.file(launcherScript).exists())) {
|
||||
if (!(await Filesystem.exists(launcherScript))) {
|
||||
log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`)
|
||||
return
|
||||
}
|
||||
@@ -1336,7 +1336,7 @@ export namespace LSPServer {
|
||||
"src",
|
||||
"server.js",
|
||||
)
|
||||
const exists = await Bun.file(js).exists()
|
||||
const exists = await Filesystem.exists(js)
|
||||
if (!exists) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
|
||||
@@ -1443,7 +1443,7 @@ export namespace LSPServer {
|
||||
}
|
||||
|
||||
const tempPath = path.join(Global.Path.bin, assetName)
|
||||
await Bun.file(tempPath).write(downloadResponse)
|
||||
if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
|
||||
|
||||
// Unlike zls which is a single self-contained binary,
|
||||
// lua-language-server needs supporting files (meta/, locale/, etc.)
|
||||
@@ -1482,7 +1482,7 @@ export namespace LSPServer {
|
||||
// Binary is located in bin/ subdirectory within the extracted archive
|
||||
bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Bun.file(bin).exists())) {
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract lua-language-server binary")
|
||||
return
|
||||
}
|
||||
@@ -1516,7 +1516,7 @@ export namespace LSPServer {
|
||||
const args: string[] = []
|
||||
if (!binary) {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "intelephense"], {
|
||||
cwd: Global.Path.bin,
|
||||
@@ -1613,7 +1613,7 @@ export namespace LSPServer {
|
||||
const args: string[] = []
|
||||
if (!binary) {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "bash-language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
@@ -1654,22 +1654,17 @@ export namespace LSPServer {
|
||||
|
||||
if (!bin) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading terraform-ls from GitHub releases")
|
||||
log.info("downloading terraform-ls from HashiCorp releases")
|
||||
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest")
|
||||
const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch terraform-ls release info")
|
||||
return
|
||||
}
|
||||
|
||||
const release = (await releaseResponse.json()) as {
|
||||
tag_name?: string
|
||||
assets?: { name?: string; browser_download_url?: string }[]
|
||||
}
|
||||
const version = release.tag_name?.replace("v", "")
|
||||
if (!version) {
|
||||
log.error("terraform-ls release did not include a version tag")
|
||||
return
|
||||
version?: string
|
||||
builds?: { arch?: string; os?: string; url?: string }[]
|
||||
}
|
||||
|
||||
const platform = process.platform
|
||||
@@ -1678,23 +1673,21 @@ export namespace LSPServer {
|
||||
const tfArch = arch === "arm64" ? "arm64" : "amd64"
|
||||
const tfPlatform = platform === "win32" ? "windows" : platform
|
||||
|
||||
const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip`
|
||||
|
||||
const assets = release.assets ?? []
|
||||
const asset = assets.find((a) => a.name === assetName)
|
||||
if (!asset?.browser_download_url) {
|
||||
log.error(`Could not find asset ${assetName} in terraform-ls release`)
|
||||
const builds = release.builds ?? []
|
||||
const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform)
|
||||
if (!build?.url) {
|
||||
log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`)
|
||||
return
|
||||
}
|
||||
|
||||
const downloadResponse = await fetch(asset.browser_download_url)
|
||||
const downloadResponse = await fetch(build.url)
|
||||
if (!downloadResponse.ok) {
|
||||
log.error("Failed to download terraform-ls")
|
||||
return
|
||||
}
|
||||
|
||||
const tempPath = path.join(Global.Path.bin, assetName)
|
||||
await Bun.file(tempPath).write(downloadResponse)
|
||||
const tempPath = path.join(Global.Path.bin, "terraform-ls.zip")
|
||||
if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
|
||||
|
||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
||||
.then(() => true)
|
||||
@@ -1707,7 +1700,7 @@ export namespace LSPServer {
|
||||
|
||||
bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Bun.file(bin).exists())) {
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract terraform-ls binary")
|
||||
return
|
||||
}
|
||||
@@ -1784,7 +1777,7 @@ export namespace LSPServer {
|
||||
}
|
||||
|
||||
const tempPath = path.join(Global.Path.bin, assetName)
|
||||
await Bun.file(tempPath).write(downloadResponse)
|
||||
if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
|
||||
|
||||
if (ext === "zip") {
|
||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
||||
@@ -1803,7 +1796,7 @@ export namespace LSPServer {
|
||||
|
||||
bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Bun.file(bin).exists())) {
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract texlab binary")
|
||||
return
|
||||
}
|
||||
@@ -1832,7 +1825,7 @@ export namespace LSPServer {
|
||||
const args: string[] = []
|
||||
if (!binary) {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
|
||||
cwd: Global.Path.bin,
|
||||
@@ -1990,7 +1983,7 @@ export namespace LSPServer {
|
||||
}
|
||||
|
||||
const tempPath = path.join(Global.Path.bin, assetName)
|
||||
await Bun.file(tempPath).write(downloadResponse)
|
||||
if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
|
||||
|
||||
if (ext === "zip") {
|
||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
||||
@@ -2008,7 +2001,7 @@ export namespace LSPServer {
|
||||
|
||||
bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Bun.file(bin).exists())) {
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract tinymist binary")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import z from "zod"
|
||||
import { Installation } from "../installation"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
// Try to import bundled snapshot (generated at build time)
|
||||
// Falls back to undefined in dev mode when snapshot doesn't exist
|
||||
@@ -85,8 +86,7 @@ export namespace ModelsDev {
|
||||
}
|
||||
|
||||
export const Data = lazy(async () => {
|
||||
const file = Bun.file(Flag.OPENCODE_MODELS_PATH ?? filepath)
|
||||
const result = await file.json().catch(() => {})
|
||||
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
|
||||
if (result) return result
|
||||
// @ts-ignore
|
||||
const snapshot = await import("./models-snapshot")
|
||||
@@ -104,7 +104,6 @@ export namespace ModelsDev {
|
||||
}
|
||||
|
||||
export async function refresh() {
|
||||
const file = Bun.file(filepath)
|
||||
const result = await fetch(`${url()}/api.json`, {
|
||||
headers: {
|
||||
"User-Agent": Installation.USER_AGENT,
|
||||
@@ -116,7 +115,7 @@ export namespace ModelsDev {
|
||||
})
|
||||
})
|
||||
if (result && result.ok) {
|
||||
await Bun.write(file, await result.text())
|
||||
await Filesystem.write(filepath, await result.text())
|
||||
ModelsDev.Data.reset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,18 +18,24 @@ export namespace Pty {
|
||||
|
||||
type Socket = {
|
||||
readyState: number
|
||||
data: object
|
||||
send: (data: string | Uint8Array<ArrayBuffer> | ArrayBuffer) => void
|
||||
close: (code?: number, reason?: string) => void
|
||||
}
|
||||
|
||||
const sockets = new WeakMap<object, number>()
|
||||
let socketCounter = 0
|
||||
// Bun's ServerWebSocket has a per-connection `.data` object (set during
|
||||
// `server.upgrade`) that changes when the underlying connection is recycled.
|
||||
// We keep a reference to a stable part of it so output can't leak even when
|
||||
// websocket objects are reused.
|
||||
const token = (ws: Socket) => {
|
||||
const data = ws.data
|
||||
const events = (data as { events?: unknown }).events
|
||||
if (events && typeof events === "object") return events
|
||||
|
||||
const tagSocket = (ws: Socket) => {
|
||||
if (!ws || typeof ws !== "object") return
|
||||
const next = (socketCounter = (socketCounter + 1) % Number.MAX_SAFE_INTEGER)
|
||||
sockets.set(ws, next)
|
||||
return next
|
||||
const url = (data as { url?: unknown }).url
|
||||
if (url && typeof url === "object") return url
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }).
|
||||
@@ -96,7 +102,7 @@ export namespace Pty {
|
||||
buffer: string
|
||||
bufferCursor: number
|
||||
cursor: number
|
||||
subscribers: Map<Socket, number>
|
||||
subscribers: Map<Socket, object>
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
@@ -176,26 +182,27 @@ export namespace Pty {
|
||||
subscribers: new Map(),
|
||||
}
|
||||
state().set(id, session)
|
||||
ptyProcess.onData((data) => {
|
||||
session.cursor += data.length
|
||||
ptyProcess.onData((chunk) => {
|
||||
session.cursor += chunk.length
|
||||
|
||||
for (const [ws, id] of session.subscribers) {
|
||||
for (const [ws, data] of session.subscribers) {
|
||||
if (ws.readyState !== 1) {
|
||||
session.subscribers.delete(ws)
|
||||
continue
|
||||
}
|
||||
if (typeof ws === "object" && sockets.get(ws) !== id) {
|
||||
|
||||
if (token(ws) !== data) {
|
||||
session.subscribers.delete(ws)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
ws.send(data)
|
||||
ws.send(chunk)
|
||||
} catch {
|
||||
session.subscribers.delete(ws)
|
||||
}
|
||||
}
|
||||
|
||||
session.buffer += data
|
||||
session.buffer += chunk
|
||||
if (session.buffer.length <= BUFFER_LIMIT) return
|
||||
const excess = session.buffer.length - BUFFER_LIMIT
|
||||
session.buffer = session.buffer.slice(excess)
|
||||
@@ -305,8 +312,12 @@ export namespace Pty {
|
||||
return
|
||||
}
|
||||
|
||||
const socketId = tagSocket(ws)
|
||||
if (typeof socketId === "number") session.subscribers.set(ws, socketId)
|
||||
if (!ws.data || typeof ws.data !== "object") {
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
|
||||
session.subscribers.set(ws, token(ws))
|
||||
return {
|
||||
onMessage: (message: string | ArrayBuffer) => {
|
||||
session.process.write(String(message))
|
||||
|
||||
@@ -163,6 +163,7 @@ export const PtyRoutes = lazy(() =>
|
||||
|
||||
type Socket = {
|
||||
readyState: number
|
||||
data: object
|
||||
send: (data: string | Uint8Array<ArrayBuffer> | ArrayBuffer) => void
|
||||
close: (code?: number, reason?: string) => void
|
||||
}
|
||||
@@ -170,6 +171,10 @@ export const PtyRoutes = lazy(() =>
|
||||
const isSocket = (value: unknown): value is Socket => {
|
||||
if (!value || typeof value !== "object") return false
|
||||
if (!("readyState" in value)) return false
|
||||
if (!("data" in value)) return false
|
||||
if (!((value as { data?: unknown }).data && typeof (value as { data?: unknown }).data === "object")) {
|
||||
return false
|
||||
}
|
||||
if (!("send" in value) || typeof (value as { send?: unknown }).send !== "function") return false
|
||||
if (!("close" in value) || typeof (value as { close?: unknown }).close !== "function") return false
|
||||
return typeof (value as { readyState?: unknown }).readyState === "number"
|
||||
@@ -177,11 +182,16 @@ export const PtyRoutes = lazy(() =>
|
||||
|
||||
return {
|
||||
onOpen(_event, ws) {
|
||||
const socket = isSocket(ws.raw) ? ws.raw : ws
|
||||
handler = Pty.connect(id, socket, cursor)
|
||||
const raw = ws.raw
|
||||
if (!isSocket(raw)) {
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
handler = Pty.connect(id, raw, cursor)
|
||||
},
|
||||
onMessage(event) {
|
||||
handler?.onMessage(String(event.data))
|
||||
if (typeof event.data !== "string") return
|
||||
handler?.onMessage(event.data)
|
||||
},
|
||||
onClose() {
|
||||
handler?.onClose()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user