mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-19 15:24:21 +00:00
Compare commits
93 Commits
github-v1.
...
chore/dupl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a75aadf02 | ||
|
|
7560913b7d | ||
|
|
ba02738d34 | ||
|
|
3a416f6f33 | ||
|
|
11a37834c2 | ||
|
|
d620455531 | ||
|
|
568eccb4c6 | ||
|
|
3a07dd8d96 | ||
|
|
802ccd3788 | ||
|
|
6042785c57 | ||
|
|
5d8664c13e | ||
|
|
3d0f24067c | ||
|
|
44049540b0 | ||
|
|
40f00ccc1c | ||
|
|
a301051263 | ||
|
|
fd61be4078 | ||
|
|
4a8bdc3c75 | ||
|
|
9c7629ce61 | ||
|
|
ba53c56a21 | ||
|
|
14c0989411 | ||
|
|
36bc07a5af | ||
|
|
270b807cdf | ||
|
|
bd52ce5640 | ||
|
|
a624871ccd | ||
|
|
819d09e64e | ||
|
|
9e6cb89101 | ||
|
|
a8347c3762 | ||
|
|
57b63ea83d | ||
|
|
c162074888 | ||
|
|
088eac9d4e | ||
|
|
5fe237a3fd | ||
|
|
ae398539c5 | ||
|
|
359360ad86 | ||
|
|
5d12eb9528 | ||
|
|
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 |
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
|
||||
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._
|
||||
|
||||
225
.github/workflows/duplicate-issues.yml
vendored
225
.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
|
||||
@@ -18,100 +19,186 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Build prompt
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require("fs")
|
||||
const issue = context.payload.issue
|
||||
const body = issue.body ?? ""
|
||||
const text = [
|
||||
"Check this new issue for compliance and duplicates:",
|
||||
"",
|
||||
`CURRENT_ISSUE_NUMBER: ${issue.number}`,
|
||||
"",
|
||||
`Title: ${issue.title}`,
|
||||
"",
|
||||
"Description:",
|
||||
body,
|
||||
].join("\n")
|
||||
|
||||
fs.writeFileSync("issue_info.txt", text)
|
||||
|
||||
- name: Check duplicates and 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-haiku-4-5 "A new issue has been created:
|
||||
bun script/duplicate-issue.ts -f issue_info.txt "Check the attached file for issue details and return either a comment body or No action required" > issue_comment.txt
|
||||
|
||||
Issue number: ${{ github.event.issue.number }}
|
||||
- name: Post comment and label issue
|
||||
env:
|
||||
COMMENT: ${{ github.workspace }}/issue_comment.txt
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require("fs")
|
||||
const comment = fs.readFileSync(process.env.COMMENT, "utf8").trim()
|
||||
|
||||
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
||||
if (comment === "No action required") {
|
||||
core.info("No comment needed")
|
||||
return
|
||||
}
|
||||
|
||||
You have TWO tasks. Perform both, then post a SINGLE comment (if needed).
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
body: `_The following comment was made by an LLM, it may be inaccurate:_\n\n${comment}`,
|
||||
})
|
||||
|
||||
---
|
||||
if (!comment.includes("<!-- issue-compliance -->")) return
|
||||
|
||||
TASK 1: CONTRIBUTING GUIDELINES COMPLIANCE CHECK
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
labels: ["needs:compliance"],
|
||||
})
|
||||
|
||||
Check whether the issue follows our contributing guidelines and issue templates.
|
||||
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
|
||||
|
||||
This project has three issue templates that every issue MUST use one of:
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
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
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
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
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
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.
|
||||
- name: Build recheck prompt
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require("fs")
|
||||
const issue = context.payload.issue
|
||||
const body = issue.body ?? ""
|
||||
const text = [
|
||||
"Recheck this edited issue for compliance:",
|
||||
"",
|
||||
"MODE: recheck-compliance",
|
||||
`CURRENT_ISSUE_NUMBER: ${issue.number}`,
|
||||
"",
|
||||
`Title: ${issue.title}`,
|
||||
"",
|
||||
"Description:",
|
||||
body,
|
||||
].join("\n")
|
||||
|
||||
---
|
||||
fs.writeFileSync("issue_info.txt", text)
|
||||
|
||||
TASK 2: DUPLICATE CHECK
|
||||
- name: Recheck compliance
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
bun script/duplicate-issue.ts -f issue_info.txt "Recheck compliance for this edited issue and return either No action required or a compliance comment body" > issue_comment.txt
|
||||
|
||||
Search through existing issues (excluding #${{ github.event.issue.number }}) to find potential duplicates.
|
||||
Consider:
|
||||
1. Similar titles or descriptions
|
||||
2. Same error messages or symptoms
|
||||
3. Related functionality or components
|
||||
4. Similar feature requests
|
||||
- name: Update compliance state
|
||||
env:
|
||||
COMMENT: ${{ github.workspace }}/issue_comment.txt
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require("fs")
|
||||
const marker = "<!-- issue-compliance -->"
|
||||
const comment = fs.readFileSync(process.env.COMMENT, "utf8").trim()
|
||||
const issue_number = context.payload.issue.number
|
||||
|
||||
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, note the pinned keybinds issue #4997.
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
---
|
||||
const compliance = comments.filter((x) => (x.body ?? "").includes(marker))
|
||||
|
||||
POSTING YOUR COMMENT:
|
||||
if (comment === "No action required") {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number,
|
||||
name: "needs:compliance",
|
||||
})
|
||||
} catch {}
|
||||
|
||||
Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows:
|
||||
for (const entry of compliance) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: entry.id,
|
||||
})
|
||||
}
|
||||
|
||||
If the issue is NOT compliant, start the comment with:
|
||||
<!-- issue-compliance -->
|
||||
Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number,
|
||||
body: "Thanks for updating your issue. It now meets our contributing guidelines. :+1:",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
If duplicates were found, include a section about potential duplicates with links.
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number,
|
||||
labels: ["needs:compliance"],
|
||||
})
|
||||
|
||||
If the issue mentions keybinds/keyboard shortcuts, include a note about #4997.
|
||||
const body = `_The following comment was made by an LLM, it may be inaccurate:_\n\n${comment}`
|
||||
const existing = compliance.at(-1)
|
||||
if (!existing) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number,
|
||||
body,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
If the issue IS compliant AND no duplicates were found AND no keybind reference, do NOT comment at all.
|
||||
|
||||
Use this format for the comment:
|
||||
|
||||
[If not compliant:]
|
||||
<!-- issue-compliance -->
|
||||
This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md).
|
||||
|
||||
**What needs to be fixed:**
|
||||
- [specific reasons]
|
||||
|
||||
Please edit this issue to address the above within **2 hours**, or it will be automatically closed.
|
||||
|
||||
[If duplicates found, add:]
|
||||
---
|
||||
This issue might be a duplicate of existing issues. Please check:
|
||||
- #[issue_number]: [brief description of similarity]
|
||||
|
||||
[If keybind-related, add:]
|
||||
For keybind-related issues, please also check our pinned keybinds documentation: #4997
|
||||
|
||||
[End with if not compliant:]
|
||||
If you believe this was flagged incorrectly, please let a maintainer know.
|
||||
|
||||
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
})
|
||||
|
||||
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 }}
|
||||
|
||||
224
.github/workflows/pr-standards.yml
vendored
224
.github/workflows/pr-standards.yml
vendored
@@ -16,9 +16,11 @@ jobs:
|
||||
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 != 'R44VC0RP' &&
|
||||
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 +28,30 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const login = pr.user.login;
|
||||
|
||||
// Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC)
|
||||
const cutoff = new Date('2026-02-19T00:00:00Z');
|
||||
const prCreated = new Date(pr.created_at);
|
||||
if (prCreated < cutoff) {
|
||||
console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 +163,201 @@ 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;
|
||||
|
||||
// Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC)
|
||||
const cutoff = new Date('2026-02-19T00:00:00Z');
|
||||
const prCreated = new Date(pr.created_at);
|
||||
if (prCreated < cutoff) {
|
||||
console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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`);
|
||||
}
|
||||
|
||||
98
.opencode/agent/duplicate-issue.md
Normal file
98
.opencode/agent/duplicate-issue.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/claude-haiku-4-5
|
||||
color: "#E67E22"
|
||||
tools:
|
||||
"*": false
|
||||
"github-issue-search": true
|
||||
---
|
||||
|
||||
You are a duplicate issue detection agent. When an issue is opened, your job is to search for potentially duplicate or related open issues.
|
||||
|
||||
You have two jobs:
|
||||
|
||||
1. Check if the issue follows our issue templates/contributing requirements.
|
||||
2. Check for potential duplicate issues.
|
||||
|
||||
Use the github-issue-search tool to find potentially related issues.
|
||||
|
||||
IMPORTANT: The input will contain a line `CURRENT_ISSUE_NUMBER: NNNN`. Never mark that issue as a duplicate of itself.
|
||||
|
||||
The input may also contain `MODE: recheck-compliance`.
|
||||
|
||||
- When MODE is `recheck-compliance`, ONLY run compliance checks. Do not run duplicate checks. Do not include duplicate or keybind sections.
|
||||
- When MODE is missing, do the full opened-issue behavior (compliance + duplicates + keybind note).
|
||||
|
||||
## Compliance checks
|
||||
|
||||
This project has three issue templates:
|
||||
|
||||
1. Bug Report - needs a Description field with real content.
|
||||
2. Feature Request - title should start with `[FEATURE]:` and include verification checkbox + meaningful description.
|
||||
3. Question - needs a Question field with real content.
|
||||
|
||||
Also check:
|
||||
|
||||
- no AI-generated walls of text
|
||||
- required sections are not placeholder-only / unchanged template text
|
||||
- bug reports include some repro context
|
||||
- feature requests explain the problem/need
|
||||
- encourage system information where relevant
|
||||
|
||||
Do not be nitpicky about optional fields. Only flag real issues (missing template/required content, placeholder-only content, obviously AI-generated wall of text, empty/nonsensical issue).
|
||||
|
||||
## Duplicate checks
|
||||
|
||||
Search for duplicates by trying multiple keyword combinations from the issue title/body. Prioritize:
|
||||
|
||||
- similar title/description
|
||||
- same error/symptoms
|
||||
- same component/feature area
|
||||
|
||||
If the issue mentions keybinds, keyboard shortcuts, or key bindings, include a note to check pinned issue #4997.
|
||||
|
||||
## Output rules
|
||||
|
||||
If MODE is `recheck-compliance` and the issue is compliant, output exactly:
|
||||
|
||||
No action required
|
||||
|
||||
If MODE is missing and the issue is compliant AND no duplicates are found AND no keybind note is needed, output exactly:
|
||||
|
||||
No action required
|
||||
|
||||
Otherwise output exactly one markdown comment body with this structure:
|
||||
|
||||
- In `recheck-compliance` mode: include only the non-compliant section and ending note.
|
||||
- In default mode: include sections as needed (non-compliant, duplicates, keybind).
|
||||
|
||||
- If non-compliant, start with:
|
||||
|
||||
<!-- issue-compliance -->
|
||||
|
||||
This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md).
|
||||
|
||||
**What needs to be fixed:**
|
||||
|
||||
- [specific reason]
|
||||
|
||||
Please edit this issue to address the above within **2 hours**, or it will be automatically closed.
|
||||
|
||||
- If duplicates were found, add:
|
||||
|
||||
---
|
||||
|
||||
This issue might be a duplicate of existing issues. Please check:
|
||||
|
||||
- #1234: [brief reason]
|
||||
|
||||
- If keybind-related, add:
|
||||
|
||||
For keybind-related issues, please also check our pinned keybinds documentation: #4997
|
||||
|
||||
- If non-compliant, end with:
|
||||
|
||||
If you believe this was flagged incorrectly, please let a maintainer know.
|
||||
|
||||
Keep output concise. Do not wrap output in code fences.
|
||||
57
.opencode/tool/github-issue-search.ts
Normal file
57
.opencode/tool/github-issue-search.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import DESCRIPTION from "./github-issue-search.txt"
|
||||
|
||||
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
interface Issue {
|
||||
title: string
|
||||
html_url: string
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description: DESCRIPTION,
|
||||
args: {
|
||||
query: tool.schema.string().describe("Search query for issue titles and descriptions"),
|
||||
limit: tool.schema.number().describe("Maximum number of results to return").default(10),
|
||||
offset: tool.schema.number().describe("Number of results to skip for pagination").default(0),
|
||||
},
|
||||
async execute(args) {
|
||||
const owner = "anomalyco"
|
||||
const repo = "opencode"
|
||||
|
||||
const page = Math.floor(args.offset / args.limit) + 1
|
||||
const searchQuery = encodeURIComponent(`${args.query} repo:${owner}/${repo} type:issue state:open`)
|
||||
const result = await githubFetch(
|
||||
`/search/issues?q=${searchQuery}&per_page=${args.limit}&page=${page}&sort=updated&order=desc`,
|
||||
)
|
||||
|
||||
if (result.total_count === 0) {
|
||||
return `No issues found matching "${args.query}"`
|
||||
}
|
||||
|
||||
const issues = result.items as Issue[]
|
||||
|
||||
if (issues.length === 0) {
|
||||
return `No other issues found matching "${args.query}"`
|
||||
}
|
||||
|
||||
const formatted = issues.map((issue) => `${issue.title}\n${issue.html_url}`).join("\n\n")
|
||||
|
||||
return `Found ${result.total_count} issues (showing ${issues.length}):\n\n${formatted}`
|
||||
},
|
||||
})
|
||||
10
.opencode/tool/github-issue-search.txt
Normal file
10
.opencode/tool/github-issue-search.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Use this tool to search GitHub issues by title and description.
|
||||
|
||||
This tool searches issues in the sst/opencode repository and returns LLM-friendly results including:
|
||||
- Issue number and title
|
||||
- Author
|
||||
- State (open/closed/merged)
|
||||
- Labels
|
||||
- Description snippet
|
||||
|
||||
Use the query parameter to search for keywords that might appear in issue titles or descriptions.
|
||||
@@ -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="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,6 +427,7 @@ export function DialogSelectServer() {
|
||||
}
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
||||
<Show
|
||||
@@ -446,8 +447,8 @@ export function DialogSelectServer() {
|
||||
>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
status={store.status[ServerConnection.key(i)]}
|
||||
dimmed={store.status[ServerConnection.key(i)]?.healthy === false}
|
||||
status={store.status[key]}
|
||||
dimmed={store.status[key]?.healthy === false}
|
||||
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultUrl() === i.http.url}>
|
||||
@@ -460,7 +461,7 @@ export function DialogSelectServer() {
|
||||
</Show>
|
||||
<Show when={store.editServer.id !== i.http.url}>
|
||||
<div class="flex items-center justify-center gap-5 pl-4">
|
||||
<Show when={current() === i}>
|
||||
<Show when={ServerConnection.key(current()) === key}>
|
||||
<p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
|
||||
</Show>
|
||||
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -102,15 +102,19 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
}),
|
||||
)
|
||||
|
||||
const allServers = createMemo(
|
||||
(): Array<ServerConnection.Any> => [
|
||||
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
||||
const servers = [
|
||||
...(props.servers ?? []),
|
||||
...store.list.map((value) => ({
|
||||
type: "http" as const,
|
||||
http: typeof value === "string" ? { url: value } : value,
|
||||
})),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
const deduped = new Map(servers.map((conn) => [ServerConnection.key(conn), conn]))
|
||||
|
||||
return [...deduped.values()]
|
||||
})
|
||||
|
||||
const [state, setState] = createStore({
|
||||
active: props.defaultServer,
|
||||
|
||||
@@ -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": "切換工作區",
|
||||
|
||||
@@ -177,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(() => {
|
||||
@@ -188,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(() => {
|
||||
@@ -225,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) => {
|
||||
@@ -1490,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)
|
||||
}
|
||||
|
||||
@@ -1924,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,4 +1,4 @@
|
||||
import { 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"
|
||||
@@ -981,7 +981,7 @@ export default function Page() {
|
||||
consumePendingMessage: layout.pendingMessage.consume,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -535,7 +535,7 @@ export function MessageTimeline(props: {
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content: "flex flex-col justify-between !overflow-visible",
|
||||
container: "w-full px-4 md:px-6",
|
||||
container: "w-full px-4 md:px-5",
|
||||
}}
|
||||
/>
|
||||
</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(
|
||||
@@ -175,11 +174,11 @@ export function SessionPromptDock(props: {
|
||||
<div
|
||||
ref={props.setPromptDockRef}
|
||||
data-component="session-prompt-dock"
|
||||
class="shrink-0 w-full pb-4 flex flex-col justify-center items-center bg-background-stronger pointer-events-none"
|
||||
class="shrink-0 w-full pb-3 flex flex-col justify-center items-center bg-background-stronger pointer-events-none"
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"w-full px-4 pointer-events-auto": true,
|
||||
"w-full px-3 pointer-events-auto": true,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
@@ -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()}
|
||||
|
||||
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()?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -168,12 +168,17 @@ function websearch(info: ToolProps<typeof WebSearchTool>) {
|
||||
}
|
||||
|
||||
function task(info: ToolProps<typeof TaskTool>) {
|
||||
const agent = Locale.titlecase(info.input.subagent_type)
|
||||
const desc = info.input.description
|
||||
const started = info.part.state.status === "running"
|
||||
const input = info.part.state.input
|
||||
const status = info.part.state.status
|
||||
const subagent =
|
||||
typeof input.subagent_type === "string" && input.subagent_type.trim().length > 0 ? input.subagent_type : "unknown"
|
||||
const agent = Locale.titlecase(subagent)
|
||||
const desc =
|
||||
typeof input.description === "string" && input.description.trim().length > 0 ? input.description : undefined
|
||||
const icon = status === "error" ? "✗" : status === "running" ? "•" : "✓"
|
||||
const name = desc ?? `${agent} Task`
|
||||
inline({
|
||||
icon: started ? "•" : "✓",
|
||||
icon,
|
||||
title: name,
|
||||
description: desc ? `${agent} Agent` : undefined,
|
||||
})
|
||||
@@ -451,9 +456,17 @@ export const RunCommand = cmd({
|
||||
const part = event.properties.part
|
||||
if (part.sessionID !== sessionID) continue
|
||||
|
||||
if (part.type === "tool" && part.state.status === "completed") {
|
||||
if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) {
|
||||
if (emit("tool_use", { part })) continue
|
||||
tool(part)
|
||||
if (part.state.status === "completed") {
|
||||
tool(part)
|
||||
continue
|
||||
}
|
||||
inline({
|
||||
icon: "✗",
|
||||
title: `${part.tool} failed`,
|
||||
})
|
||||
UI.error(part.state.error)
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -34,7 +34,6 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
||||
renderer.setTerminalTitle("")
|
||||
renderer.destroy()
|
||||
win32FlushInputBuffer()
|
||||
await input.onExit?.()
|
||||
if (reason) {
|
||||
const formatted = FormatError(reason) ?? FormatUnknownError(reason)
|
||||
if (formatted) {
|
||||
@@ -43,7 +42,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
||||
}
|
||||
const text = store.get()
|
||||
if (text) process.stdout.write(text + "\n")
|
||||
process.exit(0)
|
||||
await input.onExit?.()
|
||||
},
|
||||
{
|
||||
message: store,
|
||||
|
||||
@@ -104,6 +104,9 @@ export namespace UI {
|
||||
}
|
||||
|
||||
export function error(message: string) {
|
||||
if (message.startsWith("Error: ")) {
|
||||
message = message.slice("Error: ".length)
|
||||
}
|
||||
println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1618,7 +1618,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
const args = matchingInvocation?.args
|
||||
|
||||
const cwd = Instance.directory
|
||||
const shellEnv = await Plugin.trigger("shell.env", { cwd }, { env: {} })
|
||||
const shellEnv = await Plugin.trigger(
|
||||
"shell.env",
|
||||
{ cwd, sessionID: input.sessionID, callID: part.callID },
|
||||
{ env: {} },
|
||||
)
|
||||
const proc = spawn(shell, args, {
|
||||
cwd,
|
||||
detached: process.platform !== "win32",
|
||||
|
||||
@@ -163,7 +163,11 @@ export const BashTool = Tool.define("bash", async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const shellEnv = await Plugin.trigger("shell.env", { cwd }, { env: {} })
|
||||
const shellEnv = await Plugin.trigger(
|
||||
"shell.env",
|
||||
{ cwd, sessionID: ctx.sessionID, callID: ctx.callID },
|
||||
{ env: {} },
|
||||
)
|
||||
const proc = spawn(params.command, {
|
||||
shell,
|
||||
cwd,
|
||||
|
||||
@@ -18,12 +18,13 @@ export namespace Filesystem {
|
||||
}
|
||||
}
|
||||
|
||||
export function stat(p: string): ReturnType<typeof statSync> | undefined {
|
||||
return statSync(p, { throwIfNoEntry: false }) ?? undefined
|
||||
}
|
||||
|
||||
export async function size(p: string): Promise<number> {
|
||||
try {
|
||||
return statSync(p).size
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
const s = stat(p)?.size ?? 0
|
||||
return typeof s === "bigint" ? Number(s) : s
|
||||
}
|
||||
|
||||
export async function readText(p: string): Promise<string> {
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("pty", () => {
|
||||
|
||||
const ws = {
|
||||
readyState: 1,
|
||||
data: { events: { connection: "a" } },
|
||||
send: (data: unknown) => {
|
||||
outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
||||
},
|
||||
@@ -30,6 +31,7 @@ describe("pty", () => {
|
||||
Pty.connect(a.id, ws as any)
|
||||
|
||||
// Now "reuse" the same ws object for another connection.
|
||||
ws.data = { events: { connection: "b" } }
|
||||
ws.send = (data: unknown) => {
|
||||
outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
||||
}
|
||||
@@ -51,4 +53,48 @@ describe("pty", () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("does not leak output when Bun recycles websocket objects before re-connect", async () => {
|
||||
await using dir = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: dir.path,
|
||||
fn: async () => {
|
||||
const a = await Pty.create({ command: "cat", title: "a" })
|
||||
try {
|
||||
const outA: string[] = []
|
||||
const outB: string[] = []
|
||||
|
||||
const ws = {
|
||||
readyState: 1,
|
||||
data: { events: { connection: "a" } },
|
||||
send: (data: unknown) => {
|
||||
outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
||||
},
|
||||
close: () => {
|
||||
// no-op (simulate abrupt drop)
|
||||
},
|
||||
}
|
||||
|
||||
// Connect "a" first.
|
||||
Pty.connect(a.id, ws as any)
|
||||
outA.length = 0
|
||||
|
||||
// Simulate Bun reusing the same websocket object for another connection
|
||||
// before the new onOpen handler has a chance to tag it.
|
||||
ws.data = { events: { connection: "b" } }
|
||||
ws.send = (data: unknown) => {
|
||||
outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
||||
}
|
||||
|
||||
Pty.write(a.id, "AAA\n")
|
||||
await Bun.sleep(100)
|
||||
|
||||
expect(outB.join("")).not.toContain("AAA")
|
||||
} finally {
|
||||
await Pty.remove(a.id)
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -185,7 +185,10 @@ export interface Hooks {
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: { args: any },
|
||||
) => Promise<void>
|
||||
"shell.env"?: (input: { cwd: string }, output: { env: Record<string, string> }) => Promise<void>
|
||||
"shell.env"?: (
|
||||
input: { cwd: string; sessionID?: string; callID?: string },
|
||||
output: { env: Record<string, string> },
|
||||
) => Promise<void>
|
||||
"tool.execute.after"?: (
|
||||
input: { tool: string; sessionID: string; callID: string; args: any },
|
||||
output: {
|
||||
|
||||
@@ -6,15 +6,24 @@ import { $ } from "bun"
|
||||
const dir = new URL("..", import.meta.url).pathname
|
||||
process.chdir(dir)
|
||||
|
||||
const pkg = await import("../package.json").then((m) => m.default)
|
||||
const pkg = (await import("../package.json").then((m) => m.default)) as {
|
||||
exports: Record<string, string | object>
|
||||
}
|
||||
const original = JSON.parse(JSON.stringify(pkg))
|
||||
for (const [key, value] of Object.entries(pkg.exports)) {
|
||||
const file = value.replace("./src/", "./dist/").replace(".ts", "")
|
||||
pkg.exports[key] = {
|
||||
import: file + ".js",
|
||||
types: file + ".d.ts",
|
||||
function transformExports(exports: Record<string, string | object>) {
|
||||
for (const [key, value] of Object.entries(exports)) {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
transformExports(value as Record<string, string | object>)
|
||||
} else if (typeof value === "string") {
|
||||
const file = value.replace("./src/", "./dist/").replace(".ts", "")
|
||||
exports[key] = {
|
||||
import: file + ".js",
|
||||
types: file + ".d.ts",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transformExports(pkg.exports)
|
||||
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
|
||||
await $`bun pm pack`
|
||||
await $`npm publish *.tgz --tag ${Script.channel} --access public`
|
||||
|
||||
@@ -48,13 +48,13 @@
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
&:focus-visible:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
&:disabled {
|
||||
color: var(--text-weak);
|
||||
@@ -65,10 +65,10 @@
|
||||
}
|
||||
}
|
||||
&[data-selected="true"]:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
&[data-active="true"] {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
border: transparent;
|
||||
background-color: var(--button-secondary-base);
|
||||
color: var(--text-strong);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
box-shadow: var(--shadow-xs-border-base);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--button-secondary-hover);
|
||||
@@ -93,8 +93,6 @@
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--button-secondary-base);
|
||||
scale: 0.99;
|
||||
transition: all 150ms ease-out;
|
||||
}
|
||||
&:disabled {
|
||||
border-color: var(--border-disabled);
|
||||
@@ -104,7 +102,7 @@
|
||||
}
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-strong-base);
|
||||
color: var(--icon-base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,10 +169,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="button"].titlebar-icon[data-variant="ghost"]:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
|
||||
[data-component="button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"] {
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
|
||||
@@ -29,11 +29,10 @@
|
||||
}
|
||||
|
||||
[data-slot="collapsible-arrow-icon"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="collapsible-arrow-icon"][data-direction="right"] {
|
||||
display: inline-flex;
|
||||
color: var(--icon-weaker);
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover [data-slot="collapsible-arrow"] {
|
||||
@@ -74,12 +73,8 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-slot="collapsible-arrow-icon"][data-direction="right"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="collapsible-arrow-icon"][data-direction="down"] {
|
||||
display: inline-flex;
|
||||
[data-slot="collapsible-arrow-icon"] {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,10 +34,7 @@ function CollapsibleContent(props: ComponentProps<typeof Kobalte.Content>) {
|
||||
function CollapsibleArrow(props?: ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="collapsible-arrow" {...(props || {})}>
|
||||
<span data-slot="collapsible-arrow-icon" data-direction="right">
|
||||
<Icon name="chevron-right" size="small" />
|
||||
</span>
|
||||
<span data-slot="collapsible-arrow-icon" data-direction="down">
|
||||
<span data-slot="collapsible-arrow-icon">
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
border: transparent;
|
||||
background-color: var(--button-secondary-base);
|
||||
color: var(--text-strong);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
box-shadow: var(--shadow-xs-border-base);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--button-secondary-hover);
|
||||
@@ -84,23 +84,23 @@
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
|
||||
/* [data-slot="icon-svg"] { */
|
||||
/* color: var(--icon-hover); */
|
||||
/* } */
|
||||
}
|
||||
&:focus-visible:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
background-color: var(--surface-base-active);
|
||||
/* [data-slot="icon-svg"] { */
|
||||
/* color: var(--icon-active); */
|
||||
/* } */
|
||||
}
|
||||
&:selected:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
background-color: var(--surface-base-active);
|
||||
/* [data-slot="icon-svg"] { */
|
||||
/* color: var(--icon-selected); */
|
||||
/* } */
|
||||
@@ -168,12 +168,8 @@
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
[data-component="icon-button"].titlebar-icon[data-variant="ghost"]:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
|
||||
[data-component="icon-button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"] {
|
||||
background-color: var(--surface-base-active);
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
|
||||
[data-component="icon-button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"] [data-slot="icon-svg"] {
|
||||
@@ -181,5 +177,5 @@
|
||||
}
|
||||
|
||||
[data-component="icon-button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"]:hover:not(:disabled) {
|
||||
background-color: var(--surface-base-active);
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ const icons = {
|
||||
"layout-right-partial": `<path d="M17.082 17.0807L6.9987 17.0807V2.91406H17.082V17.0807Z" fill="currentColor" fill-opacity="16%" /><path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor" />`,
|
||||
"layout-right-full": `<path d="M17.082 17.0807L6.9987 17.0807V2.91406H17.082V17.0807Z" fill="currentColor" /><path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor" />`,
|
||||
"square-arrow-top-right": `<path d="M7.91675 2.9165H2.91675V17.0832H17.0834V12.0832M12.0834 2.9165H17.0834V7.9165M9.58342 10.4165L16.6667 3.33317" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"open-file": `<path d="M7.91602 2.91406H2.91602V17.0807H17.0827V12.0807M12.0827 2.91406H17.0827V7.91406M9.58268 10.4141L16.666 3.33073" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"speech-bubble": `<path d="M18.3334 10.0003C18.3334 5.57324 15.0927 2.91699 10.0001 2.91699C4.90749 2.91699 1.66675 5.57324 1.66675 10.0003C1.66675 11.1497 2.45578 13.1016 2.5771 13.3949C2.5878 13.4207 2.59839 13.4444 2.60802 13.4706C2.69194 13.6996 3.04282 14.9364 1.66675 16.7684C3.5186 17.6538 5.48526 16.1982 5.48526 16.1982C6.84592 16.9202 8.46491 17.0837 10.0001 17.0837C15.0927 17.0837 18.3334 14.4274 18.3334 10.0003Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
comment: `<path d="M16.25 3.75H3.75V16.25L6.875 14.4643H16.25V3.75Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"folder-add-left": `<path d="M2.08333 9.58268V2.91602H8.33333L10 5.41602H17.9167V16.2493H8.75M3.75 12.0827V14.5827M3.75 14.5827V17.0827M3.75 14.5827H1.25M3.75 14.5827H6.25" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
|
||||
@@ -227,12 +227,13 @@
|
||||
[data-component="reasoning-part"] {
|
||||
width: 100%;
|
||||
color: var(--text-base);
|
||||
opacity: 0.8;
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-large);
|
||||
|
||||
[data-component="markdown"] {
|
||||
margin-top: 24px;
|
||||
font-style: normal;
|
||||
font-size: inherit;
|
||||
|
||||
p:has(strong) {
|
||||
margin-top: 24px;
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
}
|
||||
|
||||
[data-slot="radio-group-indicator"] {
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
background: var(--button-secondary-base);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
box-shadow: var(--shadow-xs-border-base);
|
||||
content: "";
|
||||
opacity: var(--indicator-opacity, 1);
|
||||
pointer-events: none;
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
[data-slot="radio-group-item-label"] {
|
||||
color: var(--text-weak);
|
||||
cursor: pointer;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -38,10 +38,24 @@
|
||||
[data-slot="session-review-actions"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
column-gap: 12px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
[data-slot="session-review-actions"] [data-component="radio-group"] {
|
||||
[data-slot="radio-group-wrapper"],
|
||||
[data-slot="radio-group-indicator"],
|
||||
[data-slot="radio-group-item-control"] {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
|
||||
+ [data-slot="radio-group-item-label"]:hover
|
||||
[data-slot="radio-group-item-control"] {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="sticky-accordion-header"] {
|
||||
top: 40px;
|
||||
}
|
||||
@@ -51,14 +65,61 @@
|
||||
top: -40px;
|
||||
}
|
||||
|
||||
[data-slot="accordion-trigger"] {
|
||||
background-color: var(--background-stronger) !important;
|
||||
}
|
||||
[data-slot="session-review-diffs-group"] {
|
||||
background-color: var(--background-stronger);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
overflow: clip;
|
||||
|
||||
[data-slot="session-review-accordion-item"][data-selected] {
|
||||
[data-slot="session-review-accordion-content"] {
|
||||
box-shadow: var(--shadow-xs-border-select);
|
||||
border-radius: var(--radius-lg);
|
||||
[data-component="accordion"] {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
[data-component="accordion"] [data-slot="accordion-item"] {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-component="accordion"]
|
||||
[data-slot="accordion-item"]
|
||||
[data-slot="accordion-header"]
|
||||
[data-slot="accordion-trigger"] {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="accordion"]
|
||||
[data-slot="accordion-item"]
|
||||
+ [data-slot="accordion-item"]
|
||||
[data-slot="accordion-header"]
|
||||
[data-slot="accordion-trigger"] {
|
||||
border-top: 1px solid var(--border-weak-base);
|
||||
}
|
||||
|
||||
[data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border-weak-base);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[data-component="sticky-accordion-header"][data-expanded]::before,
|
||||
[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
[data-slot="session-review-accordion-item"][data-selected]
|
||||
[data-slot="accordion-header"]
|
||||
[data-slot="accordion-trigger"] {
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,12 +157,13 @@
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="session-review-file-name-container"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -132,6 +194,8 @@
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
transform: translateZ(0);
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
@@ -152,6 +216,23 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
[data-slot="session-review-diff-chevron"] {
|
||||
display: inline-flex;
|
||||
color: var(--icon-weaker);
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
[data-slot="accordion-item"][data-expanded] [data-slot="session-review-diff-chevron"] {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
[data-slot="session-review-change-group"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-slot="session-review-change"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FileIcon } from "./file-icon"
|
||||
import { Icon } from "./icon"
|
||||
import { LineComment, LineCommentEditor } from "./line-comment"
|
||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||
import { Tooltip } from "./tooltip"
|
||||
import { useDiffComponent } from "../context/diff"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
@@ -286,18 +287,14 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-slot="session-review-header"
|
||||
classList={{
|
||||
[props.classes?.header ?? ""]: !!props.classes?.header,
|
||||
}}
|
||||
>
|
||||
<div data-slot="session-review-header" class={props.classes?.header}>
|
||||
<div data-slot="session-review-title">{props.title ?? i18n.t("ui.sessionReview.title")}</div>
|
||||
<div data-slot="session-review-actions">
|
||||
<Show when={hasDiffs() && props.onDiffStyleChange}>
|
||||
<RadioGroup
|
||||
options={["unified", "split"] as const}
|
||||
current={diffStyle()}
|
||||
size="small"
|
||||
value={(style) => style}
|
||||
label={(style) =>
|
||||
i18n.t(style === "unified" ? "ui.sessionReview.diffStyle.unified" : "ui.sessionReview.diffStyle.split")
|
||||
@@ -306,7 +303,12 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
/>
|
||||
</Show>
|
||||
<Show when={hasDiffs()}>
|
||||
<Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
|
||||
<Button
|
||||
size="small"
|
||||
icon="chevron-grabber-vertical"
|
||||
class="w-[106px] justify-start"
|
||||
onClick={handleExpandOrCollapseAll}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={open().length > 0}>{i18n.t("ui.sessionReview.collapseAll")}</Match>
|
||||
<Match when={true}>{i18n.t("ui.sessionReview.expandAll")}</Match>
|
||||
@@ -316,392 +318,397 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
{props.actions}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-slot="session-review-container"
|
||||
classList={{
|
||||
[props.classes?.container ?? ""]: !!props.classes?.container,
|
||||
}}
|
||||
>
|
||||
<div data-slot="session-review-container" class={props.classes?.container}>
|
||||
<Show when={hasDiffs()} fallback={props.empty}>
|
||||
<Accordion multiple value={open()} onChange={handleChange}>
|
||||
<For each={props.diffs}>
|
||||
{(diff) => {
|
||||
let wrapper: HTMLDivElement | undefined
|
||||
<div data-slot="session-review-diffs-group">
|
||||
<Accordion multiple value={open()} onChange={handleChange}>
|
||||
<For each={props.diffs}>
|
||||
{(diff) => {
|
||||
let wrapper: HTMLDivElement | undefined
|
||||
|
||||
const expanded = createMemo(() => open().includes(diff.file))
|
||||
const [force, setForce] = createSignal(false)
|
||||
const expanded = createMemo(() => open().includes(diff.file))
|
||||
const [force, setForce] = createSignal(false)
|
||||
|
||||
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
|
||||
const commentedLines = createMemo(() => comments().map((c) => c.selection))
|
||||
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
|
||||
const commentedLines = createMemo(() => comments().map((c) => c.selection))
|
||||
|
||||
const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
|
||||
const afterText = () => (typeof diff.after === "string" ? diff.after : "")
|
||||
const changedLines = () => diff.additions + diff.deletions
|
||||
const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
|
||||
const afterText = () => (typeof diff.after === "string" ? diff.after : "")
|
||||
const changedLines = () => diff.additions + diff.deletions
|
||||
|
||||
const tooLarge = createMemo(() => {
|
||||
if (!expanded()) return false
|
||||
if (force()) return false
|
||||
if (isImageFile(diff.file)) return false
|
||||
return changedLines() > MAX_DIFF_CHANGED_LINES
|
||||
})
|
||||
const tooLarge = createMemo(() => {
|
||||
if (!expanded()) return false
|
||||
if (force()) return false
|
||||
if (isImageFile(diff.file)) return false
|
||||
return changedLines() > MAX_DIFF_CHANGED_LINES
|
||||
})
|
||||
|
||||
const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
|
||||
const isDeleted = () =>
|
||||
diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
|
||||
const isImage = () => isImageFile(diff.file)
|
||||
const isAudio = () => isAudioFile(diff.file)
|
||||
const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
|
||||
const isDeleted = () =>
|
||||
diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
|
||||
const isImage = () => isImageFile(diff.file)
|
||||
const isAudio = () => isAudioFile(diff.file)
|
||||
|
||||
const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||
const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
|
||||
const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||
const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||
const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
|
||||
const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||
|
||||
const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||
const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
|
||||
const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||
const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)
|
||||
const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||
const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
|
||||
const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||
const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)
|
||||
|
||||
const selectedLines = createMemo(() => {
|
||||
const current = selection()
|
||||
if (!current || current.file !== diff.file) return null
|
||||
return current.range
|
||||
})
|
||||
const selectedLines = createMemo(() => {
|
||||
const current = selection()
|
||||
if (!current || current.file !== diff.file) return null
|
||||
return current.range
|
||||
})
|
||||
|
||||
const draftRange = createMemo(() => {
|
||||
const current = commenting()
|
||||
if (!current || current.file !== diff.file) return null
|
||||
return current.range
|
||||
})
|
||||
const draftRange = createMemo(() => {
|
||||
const current = commenting()
|
||||
if (!current || current.file !== diff.file) return null
|
||||
return current.range
|
||||
})
|
||||
|
||||
const [draft, setDraft] = createSignal("")
|
||||
const [positions, setPositions] = createSignal<Record<string, number>>({})
|
||||
const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
|
||||
const [draft, setDraft] = createSignal("")
|
||||
const [positions, setPositions] = createSignal<Record<string, number>>({})
|
||||
const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
|
||||
|
||||
const getRoot = () => {
|
||||
const el = wrapper
|
||||
if (!el) return
|
||||
const getRoot = () => {
|
||||
const el = wrapper
|
||||
if (!el) return
|
||||
|
||||
const host = el.querySelector("diffs-container")
|
||||
if (!(host instanceof HTMLElement)) return
|
||||
return host.shadowRoot ?? undefined
|
||||
}
|
||||
|
||||
const updateAnchors = () => {
|
||||
const el = wrapper
|
||||
if (!el) return
|
||||
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const next: Record<string, number> = {}
|
||||
for (const item of comments()) {
|
||||
const marker = findMarker(root, item.selection)
|
||||
if (!marker) continue
|
||||
next[item.id] = markerTop(el, marker)
|
||||
}
|
||||
setPositions(next)
|
||||
|
||||
const range = draftRange()
|
||||
if (!range) {
|
||||
setDraftTop(undefined)
|
||||
return
|
||||
const host = el.querySelector("diffs-container")
|
||||
if (!(host instanceof HTMLElement)) return
|
||||
return host.shadowRoot ?? undefined
|
||||
}
|
||||
|
||||
const marker = findMarker(root, range)
|
||||
if (!marker) {
|
||||
setDraftTop(undefined)
|
||||
return
|
||||
const updateAnchors = () => {
|
||||
const el = wrapper
|
||||
if (!el) return
|
||||
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const next: Record<string, number> = {}
|
||||
for (const item of comments()) {
|
||||
const marker = findMarker(root, item.selection)
|
||||
if (!marker) continue
|
||||
next[item.id] = markerTop(el, marker)
|
||||
}
|
||||
setPositions(next)
|
||||
|
||||
const range = draftRange()
|
||||
if (!range) {
|
||||
setDraftTop(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const marker = findMarker(root, range)
|
||||
if (!marker) {
|
||||
setDraftTop(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
setDraftTop(markerTop(el, marker))
|
||||
}
|
||||
|
||||
setDraftTop(markerTop(el, marker))
|
||||
}
|
||||
const scheduleAnchors = () => {
|
||||
requestAnimationFrame(updateAnchors)
|
||||
}
|
||||
|
||||
const scheduleAnchors = () => {
|
||||
requestAnimationFrame(updateAnchors)
|
||||
}
|
||||
createEffect(() => {
|
||||
comments()
|
||||
scheduleAnchors()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
comments()
|
||||
scheduleAnchors()
|
||||
})
|
||||
createEffect(() => {
|
||||
const range = draftRange()
|
||||
if (!range) return
|
||||
setDraft("")
|
||||
scheduleAnchors()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const range = draftRange()
|
||||
if (!range) return
|
||||
setDraft("")
|
||||
scheduleAnchors()
|
||||
})
|
||||
createEffect(() => {
|
||||
if (!open().includes(diff.file)) return
|
||||
if (!isImage()) return
|
||||
if (imageSrc()) return
|
||||
if (imageStatus() !== "idle") return
|
||||
if (isDeleted()) return
|
||||
|
||||
createEffect(() => {
|
||||
if (!open().includes(diff.file)) return
|
||||
if (!isImage()) return
|
||||
if (imageSrc()) return
|
||||
if (imageStatus() !== "idle") return
|
||||
if (isDeleted()) return
|
||||
const reader = props.readFile
|
||||
if (!reader) return
|
||||
|
||||
const reader = props.readFile
|
||||
if (!reader) return
|
||||
|
||||
setImageStatus("loading")
|
||||
reader(diff.file)
|
||||
.then((result) => {
|
||||
const src = dataUrl(result)
|
||||
if (!src) {
|
||||
setImageStatus("loading")
|
||||
reader(diff.file)
|
||||
.then((result) => {
|
||||
const src = dataUrl(result)
|
||||
if (!src) {
|
||||
setImageStatus("error")
|
||||
return
|
||||
}
|
||||
setImageSrc(src)
|
||||
setImageStatus("idle")
|
||||
})
|
||||
.catch(() => {
|
||||
setImageStatus("error")
|
||||
return
|
||||
}
|
||||
setImageSrc(src)
|
||||
setImageStatus("idle")
|
||||
})
|
||||
.catch(() => {
|
||||
setImageStatus("error")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!open().includes(diff.file)) return
|
||||
if (!isAudio()) return
|
||||
if (audioSrc()) return
|
||||
if (audioStatus() !== "idle") return
|
||||
createEffect(() => {
|
||||
if (!open().includes(diff.file)) return
|
||||
if (!isAudio()) return
|
||||
if (audioSrc()) return
|
||||
if (audioStatus() !== "idle") return
|
||||
|
||||
const reader = props.readFile
|
||||
if (!reader) return
|
||||
const reader = props.readFile
|
||||
if (!reader) return
|
||||
|
||||
setAudioStatus("loading")
|
||||
reader(diff.file)
|
||||
.then((result) => {
|
||||
const src = dataUrl(result)
|
||||
if (!src) {
|
||||
setAudioStatus("loading")
|
||||
reader(diff.file)
|
||||
.then((result) => {
|
||||
const src = dataUrl(result)
|
||||
if (!src) {
|
||||
setAudioStatus("error")
|
||||
return
|
||||
}
|
||||
setAudioMime(normalizeMimeType(result?.mimeType))
|
||||
setAudioSrc(src)
|
||||
setAudioStatus("idle")
|
||||
})
|
||||
.catch(() => {
|
||||
setAudioStatus("error")
|
||||
return
|
||||
}
|
||||
setAudioMime(normalizeMimeType(result?.mimeType))
|
||||
setAudioSrc(src)
|
||||
setAudioStatus("idle")
|
||||
})
|
||||
.catch(() => {
|
||||
setAudioStatus("error")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const handleLineSelected = (range: SelectedLineRange | null) => {
|
||||
if (!props.onLineComment) return
|
||||
const handleLineSelected = (range: SelectedLineRange | null) => {
|
||||
if (!props.onLineComment) return
|
||||
|
||||
if (!range) {
|
||||
setSelection(null)
|
||||
return
|
||||
if (!range) {
|
||||
setSelection(null)
|
||||
return
|
||||
}
|
||||
|
||||
setSelection({ file: diff.file, range })
|
||||
}
|
||||
|
||||
setSelection({ file: diff.file, range })
|
||||
}
|
||||
const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
|
||||
if (!props.onLineComment) return
|
||||
|
||||
const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
|
||||
if (!props.onLineComment) return
|
||||
if (!range) {
|
||||
setCommenting(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (!range) {
|
||||
setCommenting(null)
|
||||
return
|
||||
setSelection({ file: diff.file, range })
|
||||
setCommenting({ file: diff.file, range })
|
||||
}
|
||||
|
||||
setSelection({ file: diff.file, range })
|
||||
setCommenting({ file: diff.file, range })
|
||||
}
|
||||
const openComment = (comment: SessionReviewComment) => {
|
||||
setOpened({ file: comment.file, id: comment.id })
|
||||
setSelection({ file: comment.file, range: comment.selection })
|
||||
}
|
||||
|
||||
const openComment = (comment: SessionReviewComment) => {
|
||||
setOpened({ file: comment.file, id: comment.id })
|
||||
setSelection({ file: comment.file, range: comment.selection })
|
||||
}
|
||||
const isCommentOpen = (comment: SessionReviewComment) => {
|
||||
const current = opened()
|
||||
if (!current) return false
|
||||
return current.file === comment.file && current.id === comment.id
|
||||
}
|
||||
|
||||
const isCommentOpen = (comment: SessionReviewComment) => {
|
||||
const current = opened()
|
||||
if (!current) return false
|
||||
return current.file === comment.file && current.id === comment.id
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion.Item
|
||||
value={diff.file}
|
||||
id={diffId(diff.file)}
|
||||
data-file={diff.file}
|
||||
data-slot="session-review-accordion-item"
|
||||
data-selected={props.focusedFile === diff.file ? "" : undefined}
|
||||
>
|
||||
<StickyAccordionHeader>
|
||||
<Accordion.Trigger>
|
||||
<div data-slot="session-review-trigger-content">
|
||||
<div data-slot="session-review-file-info">
|
||||
<FileIcon node={{ path: diff.file, type: "file" }} />
|
||||
<div data-slot="session-review-file-name-container">
|
||||
<Show when={diff.file.includes("/")}>
|
||||
<span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
|
||||
</Show>
|
||||
<span data-slot="session-review-filename">{getFilename(diff.file)}</span>
|
||||
<Show when={props.onViewFile}>
|
||||
<button
|
||||
data-slot="session-review-view-button"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.onViewFile?.(diff.file)
|
||||
}}
|
||||
>
|
||||
<Icon name="eye" size="small" />
|
||||
</button>
|
||||
</Show>
|
||||
return (
|
||||
<Accordion.Item
|
||||
value={diff.file}
|
||||
id={diffId(diff.file)}
|
||||
data-file={diff.file}
|
||||
data-slot="session-review-accordion-item"
|
||||
data-selected={props.focusedFile === diff.file ? "" : undefined}
|
||||
>
|
||||
<StickyAccordionHeader>
|
||||
<Accordion.Trigger>
|
||||
<div data-slot="session-review-trigger-content">
|
||||
<div data-slot="session-review-file-info">
|
||||
<FileIcon node={{ path: diff.file, type: "file" }} />
|
||||
<div data-slot="session-review-file-name-container">
|
||||
<Show when={diff.file.includes("/")}>
|
||||
<span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
|
||||
</Show>
|
||||
<span data-slot="session-review-filename">{getFilename(diff.file)}</span>
|
||||
<Show when={props.onViewFile}>
|
||||
<Tooltip value="Open file" placement="top" gutter={4}>
|
||||
<button
|
||||
data-slot="session-review-view-button"
|
||||
type="button"
|
||||
aria-label="Open file"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.onViewFile?.(diff.file)
|
||||
}}
|
||||
>
|
||||
<Icon name="open-file" size="small" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="session-review-trigger-actions">
|
||||
<Switch>
|
||||
<Match when={isAdded()}>
|
||||
<div data-slot="session-review-change-group" data-type="added">
|
||||
<span data-slot="session-review-change" data-type="added">
|
||||
{i18n.t("ui.sessionReview.change.added")}
|
||||
</span>
|
||||
<DiffChanges changes={diff} />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isDeleted()}>
|
||||
<span data-slot="session-review-change" data-type="removed">
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={isImage()}>
|
||||
<span data-slot="session-review-change" data-type="modified">
|
||||
{i18n.t("ui.sessionReview.change.modified")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<DiffChanges changes={diff} />
|
||||
</Match>
|
||||
</Switch>
|
||||
<span data-slot="session-review-diff-chevron">
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="session-review-trigger-actions">
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content data-slot="session-review-accordion-content">
|
||||
<div
|
||||
data-slot="session-review-diff-wrapper"
|
||||
ref={(el) => {
|
||||
wrapper = el
|
||||
anchors.set(diff.file, el)
|
||||
scheduleAnchors()
|
||||
}}
|
||||
>
|
||||
<Show when={expanded()}>
|
||||
<Switch>
|
||||
<Match when={isAdded()}>
|
||||
<span data-slot="session-review-change" data-type="added">
|
||||
{i18n.t("ui.sessionReview.change.added")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={isDeleted()}>
|
||||
<span data-slot="session-review-change" data-type="removed">
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={isImage()}>
|
||||
<span data-slot="session-review-change" data-type="modified">
|
||||
{i18n.t("ui.sessionReview.change.modified")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<DiffChanges changes={diff} />
|
||||
</Match>
|
||||
</Switch>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content data-slot="session-review-accordion-content">
|
||||
<div
|
||||
data-slot="session-review-diff-wrapper"
|
||||
ref={(el) => {
|
||||
wrapper = el
|
||||
anchors.set(diff.file, el)
|
||||
scheduleAnchors()
|
||||
}}
|
||||
>
|
||||
<Show when={expanded()}>
|
||||
<Switch>
|
||||
<Match when={isImage() && imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && isDeleted()}>
|
||||
<div data-slot="session-review-image-container" data-removed>
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && !imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{imageStatus() === "loading"
|
||||
? i18n.t("ui.sessionReview.image.loading")
|
||||
: i18n.t("ui.sessionReview.image.placeholder")}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!isImage() && tooLarge()}>
|
||||
<div data-slot="session-review-large-diff">
|
||||
<div data-slot="session-review-large-diff-title">
|
||||
{i18n.t("ui.sessionReview.largeDiff.title")}
|
||||
<Match when={isImage() && imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
|
||||
</div>
|
||||
<div data-slot="session-review-large-diff-meta">
|
||||
{i18n.t("ui.sessionReview.largeDiff.meta", {
|
||||
limit: MAX_DIFF_CHANGED_LINES.toLocaleString(),
|
||||
current: changedLines().toLocaleString(),
|
||||
})}
|
||||
</Match>
|
||||
<Match when={isImage() && isDeleted()}>
|
||||
<div data-slot="session-review-image-container" data-removed>
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</div>
|
||||
<div data-slot="session-review-large-diff-actions">
|
||||
<Button size="normal" variant="secondary" onClick={() => setForce(true)}>
|
||||
{i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
|
||||
</Button>
|
||||
</Match>
|
||||
<Match when={isImage() && !imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{imageStatus() === "loading"
|
||||
? i18n.t("ui.sessionReview.image.loading")
|
||||
: i18n.t("ui.sessionReview.image.placeholder")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!isImage()}>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
preloadedDiff={diff.preloaded}
|
||||
diffStyle={diffStyle()}
|
||||
onRendered={() => {
|
||||
props.onDiffRendered?.()
|
||||
scheduleAnchors()
|
||||
}}
|
||||
enableLineSelection={props.onLineComment != null}
|
||||
onLineSelected={handleLineSelected}
|
||||
onLineSelectionEnd={handleLineSelectionEnd}
|
||||
selectedLines={selectedLines()}
|
||||
commentedLines={commentedLines()}
|
||||
before={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.before === "string" ? diff.before : "",
|
||||
}}
|
||||
after={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.after === "string" ? diff.after : "",
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<For each={comments()}>
|
||||
{(comment) => (
|
||||
<LineComment
|
||||
id={comment.id}
|
||||
top={positions()[comment.id]}
|
||||
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
|
||||
onClick={() => {
|
||||
if (isCommentOpen(comment)) {
|
||||
setOpened(null)
|
||||
return
|
||||
}
|
||||
|
||||
openComment(comment)
|
||||
}}
|
||||
open={isCommentOpen(comment)}
|
||||
comment={comment.comment}
|
||||
selection={selectionLabel(comment.selection)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={draftRange()}>
|
||||
{(range) => (
|
||||
<Show when={draftTop() !== undefined}>
|
||||
<LineCommentEditor
|
||||
top={draftTop()}
|
||||
value={draft()}
|
||||
selection={selectionLabel(range())}
|
||||
onInput={setDraft}
|
||||
onCancel={() => setCommenting(null)}
|
||||
onSubmit={(comment) => {
|
||||
props.onLineComment?.({
|
||||
file: diff.file,
|
||||
selection: range(),
|
||||
comment,
|
||||
preview: selectionPreview(diff, range()),
|
||||
})
|
||||
setCommenting(null)
|
||||
</Match>
|
||||
<Match when={!isImage() && tooLarge()}>
|
||||
<div data-slot="session-review-large-diff">
|
||||
<div data-slot="session-review-large-diff-title">
|
||||
{i18n.t("ui.sessionReview.largeDiff.title")}
|
||||
</div>
|
||||
<div data-slot="session-review-large-diff-meta">
|
||||
{i18n.t("ui.sessionReview.largeDiff.meta", {
|
||||
limit: MAX_DIFF_CHANGED_LINES.toLocaleString(),
|
||||
current: changedLines().toLocaleString(),
|
||||
})}
|
||||
</div>
|
||||
<div data-slot="session-review-large-diff-actions">
|
||||
<Button size="normal" variant="secondary" onClick={() => setForce(true)}>
|
||||
{i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!isImage()}>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
preloadedDiff={diff.preloaded}
|
||||
diffStyle={diffStyle()}
|
||||
onRendered={() => {
|
||||
props.onDiffRendered?.()
|
||||
scheduleAnchors()
|
||||
}}
|
||||
enableLineSelection={props.onLineComment != null}
|
||||
onLineSelected={handleLineSelected}
|
||||
onLineSelectionEnd={handleLineSelectionEnd}
|
||||
selectedLines={selectedLines()}
|
||||
commentedLines={commentedLines()}
|
||||
before={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.before === "string" ? diff.before : "",
|
||||
}}
|
||||
after={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.after === "string" ? diff.after : "",
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<For each={comments()}>
|
||||
{(comment) => (
|
||||
<LineComment
|
||||
id={comment.id}
|
||||
top={positions()[comment.id]}
|
||||
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
|
||||
onClick={() => {
|
||||
if (isCommentOpen(comment)) {
|
||||
setOpened(null)
|
||||
return
|
||||
}
|
||||
|
||||
openComment(comment)
|
||||
}}
|
||||
open={isCommentOpen(comment)}
|
||||
comment={comment.comment}
|
||||
selection={selectionLabel(comment.selection)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={draftRange()}>
|
||||
{(range) => (
|
||||
<Show when={draftTop() !== undefined}>
|
||||
<LineCommentEditor
|
||||
top={draftTop()}
|
||||
value={draft()}
|
||||
selection={selectionLabel(range())}
|
||||
onInput={setDraft}
|
||||
onCancel={() => setCommenting(null)}
|
||||
onSubmit={(comment) => {
|
||||
props.onLineComment?.({
|
||||
file: diff.file,
|
||||
selection: range(),
|
||||
comment,
|
||||
preview: selectionPreview(diff, range()),
|
||||
})
|
||||
setCommenting(null)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Accordion>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Accordion>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -127,7 +127,49 @@
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-diffs-group"] {
|
||||
background-color: var(--background-stronger);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
overflow: clip;
|
||||
|
||||
[data-component="accordion"] {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
[data-component="accordion"]
|
||||
[data-slot="accordion-item"]
|
||||
[data-slot="accordion-header"]
|
||||
[data-slot="accordion-trigger"] {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="accordion"]
|
||||
[data-slot="accordion-item"]
|
||||
+ [data-slot="accordion-item"]
|
||||
[data-slot="accordion-header"]
|
||||
[data-slot="accordion-trigger"] {
|
||||
border-top: 1px solid var(--border-weak-base);
|
||||
}
|
||||
|
||||
[data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border-weak-base);
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-diff-trigger"] {
|
||||
@@ -140,12 +182,11 @@
|
||||
}
|
||||
|
||||
[data-slot="session-turn-diff-path"] {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-large);
|
||||
@@ -153,6 +194,13 @@
|
||||
|
||||
[data-slot="session-turn-diff-directory"] {
|
||||
color: var(--text-weak);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
direction: rtl;
|
||||
unicode-bidi: plaintext;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-diff-filename"] {
|
||||
|
||||
@@ -315,76 +315,78 @@ export function SessionTurn(
|
||||
<Collapsible.Content>
|
||||
<Show when={open()}>
|
||||
<div data-component="session-turn-diffs-content">
|
||||
<Accordion
|
||||
multiple
|
||||
value={expanded()}
|
||||
onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])}
|
||||
>
|
||||
<For each={diffs()}>
|
||||
{(diff) => {
|
||||
const active = createMemo(() => expanded().includes(diff.file))
|
||||
const [visible, setVisible] = createSignal(false)
|
||||
<div data-slot="session-turn-diffs-group">
|
||||
<Accordion
|
||||
multiple
|
||||
value={expanded()}
|
||||
onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])}
|
||||
>
|
||||
<For each={diffs()}>
|
||||
{(diff) => {
|
||||
const active = createMemo(() => expanded().includes(diff.file))
|
||||
const [visible, setVisible] = createSignal(false)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
active,
|
||||
(value) => {
|
||||
if (!value) {
|
||||
setVisible(false)
|
||||
return
|
||||
}
|
||||
createEffect(
|
||||
on(
|
||||
active,
|
||||
(value) => {
|
||||
if (!value) {
|
||||
setVisible(false)
|
||||
return
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!active()) return
|
||||
setVisible(true)
|
||||
})
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
requestAnimationFrame(() => {
|
||||
if (!active()) return
|
||||
setVisible(true)
|
||||
})
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
return (
|
||||
<Accordion.Item value={diff.file}>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger>
|
||||
<div data-slot="session-turn-diff-trigger">
|
||||
<span data-slot="session-turn-diff-path">
|
||||
<Show when={diff.file.includes("/")}>
|
||||
<span data-slot="session-turn-diff-directory">
|
||||
{getDirectory(diff.file)}
|
||||
return (
|
||||
<Accordion.Item value={diff.file}>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger>
|
||||
<div data-slot="session-turn-diff-trigger">
|
||||
<span data-slot="session-turn-diff-path">
|
||||
<Show when={diff.file.includes("/")}>
|
||||
<span data-slot="session-turn-diff-directory">
|
||||
{`\u202A${getDirectory(diff.file)}\u202C`}
|
||||
</span>
|
||||
</Show>
|
||||
<span data-slot="session-turn-diff-filename">
|
||||
{getFilename(diff.file)}
|
||||
</span>
|
||||
</Show>
|
||||
<span data-slot="session-turn-diff-filename">
|
||||
{getFilename(diff.file)}
|
||||
</span>
|
||||
</span>
|
||||
<div data-slot="session-turn-diff-meta">
|
||||
<span data-slot="session-turn-diff-changes">
|
||||
<DiffChanges changes={diff} />
|
||||
</span>
|
||||
<span data-slot="session-turn-diff-chevron">
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</span>
|
||||
<div data-slot="session-turn-diff-meta">
|
||||
<span data-slot="session-turn-diff-changes">
|
||||
<DiffChanges changes={diff} />
|
||||
</span>
|
||||
<span data-slot="session-turn-diff-chevron">
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content>
|
||||
<Show when={visible()}>
|
||||
<div data-slot="session-turn-diff-view" data-scrollable>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
before={{ name: diff.file, contents: diff.before }}
|
||||
after={{ name: diff.file, contents: diff.after }}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Accordion>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content>
|
||||
<Show when={visible()}>
|
||||
<div data-slot="session-turn-diff-view" data-scrollable>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
before={{ name: diff.file, contents: diff.before }}
|
||||
after={{ name: diff.file, contents: diff.after }}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Collapsible.Content>
|
||||
|
||||
@@ -253,16 +253,20 @@
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
background-color: var(--surface-base-active);
|
||||
color: var(--text-strong);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,10 +278,11 @@
|
||||
padding-inline: 12px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
background-color: var(--background-stronger);
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
height: 26px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
color: var(--text-weak);
|
||||
|
||||
@@ -325,11 +330,11 @@
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
background-color: var(--surface-base-active);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
@@ -363,11 +368,11 @@
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
@@ -426,16 +431,16 @@
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
|
||||
&:has([data-slot="tabs-trigger"]:focus-visible) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
background-color: var(--surface-base-hover);
|
||||
box-shadow: var(--shadow-xs-border-focus);
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
background-color: var(--surface-base-active);
|
||||
color: var(--text-strong);
|
||||
|
||||
[data-component="icon"] {
|
||||
@@ -443,13 +448,13 @@
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-content"] {
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
background-color: var(--surface-stronger-non-alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,100 @@
|
||||
:root {
|
||||
--gray-dark-1: #161616;
|
||||
--gray-dark-2: #1c1c1c;
|
||||
--gray-dark-3: #232323;
|
||||
--gray-dark-4: #282828;
|
||||
--gray-dark-5: #2e2e2e;
|
||||
--gray-dark-6: #343434;
|
||||
--gray-dark-7: #3e3e3e;
|
||||
--gray-dark-8: #505050;
|
||||
--gray-dark-9: #707070;
|
||||
--gray-dark-10: #7e7e7e;
|
||||
--gray-dark-11: #a0a0a0;
|
||||
--gray-dark-12: #ededed;
|
||||
--gray-light-1: #fcfcfc;
|
||||
--gray-light-2: #f8f8f8;
|
||||
--gray-light-3: #f3f3f3;
|
||||
--gray-light-4: #ededed;
|
||||
--gray-light-5: #e8e8e8;
|
||||
--gray-light-6: #e2e2e2;
|
||||
--gray-light-7: #dbdbdb;
|
||||
--gray-light-8: #c7c7c7;
|
||||
--gray-light-9: #8f8f8f;
|
||||
--gray-light-10: #858585;
|
||||
--gray-light-11: #6f6f6f;
|
||||
--gray-light-12: #171717;
|
||||
--gray-dark-alpha-1: #00000000;
|
||||
--gray-dark-alpha-2: #ffffff08;
|
||||
--gray-dark-alpha-3: #ffffff0f;
|
||||
--gray-dark-alpha-4: #ffffff14;
|
||||
--gray-dark-alpha-5: #ffffff1a;
|
||||
--gray-dark-alpha-6: #ffffff21;
|
||||
--gray-dark-alpha-7: #ffffff2b;
|
||||
--gray-dark-alpha-8: #ffffff40;
|
||||
--gray-dark-alpha-9: #ffffff63;
|
||||
--gray-dark-alpha-10: #ffffff73;
|
||||
--gray-dark-alpha-11: #ffffff96;
|
||||
--gray-dark-alpha-12: #ffffffeb;
|
||||
--gray-light-alpha-1: #00000003;
|
||||
--gray-light-alpha-2: #00000008;
|
||||
--gray-light-alpha-3: #0000000d;
|
||||
--gray-light-alpha-4: #00000012;
|
||||
--gray-light-alpha-5: #00000017;
|
||||
--gray-light-alpha-6: #0000001c;
|
||||
--gray-light-alpha-7: #00000024;
|
||||
--gray-light-alpha-8: #00000038;
|
||||
--gray-light-alpha-9: #00000070;
|
||||
--gray-light-alpha-10: #0000007a;
|
||||
--gray-light-alpha-11: #0000008f;
|
||||
--gray-light-alpha-12: #000000e8;
|
||||
--gray-dark-1: #131010;
|
||||
--gray-dark-2: #1b1818;
|
||||
--gray-dark-3: #252121;
|
||||
--gray-dark-4: #2d2828;
|
||||
--gray-dark-5: #343030;
|
||||
--gray-dark-6: #3e3939;
|
||||
--gray-dark-7: #4b4646;
|
||||
--gray-dark-8: #645f5f;
|
||||
--gray-dark-9: #716c6b;
|
||||
--gray-dark-10: #7f7979;
|
||||
--gray-dark-11: #b7b1b1;
|
||||
--gray-dark-12: #f1ecec;
|
||||
--gray-light-1: #fdfcfc;
|
||||
--gray-light-2: #f9f8f8;
|
||||
--gray-light-3: #f1f0f0;
|
||||
--gray-light-4: #e9e8e8;
|
||||
--gray-light-5: #e2e0e0;
|
||||
--gray-light-6: #dad9d9;
|
||||
--gray-light-7: #cfcecd;
|
||||
--gray-light-8: #bcbbbb;
|
||||
--gray-light-9: #8e8b8b;
|
||||
--gray-light-10: #848181;
|
||||
--gray-light-11: #656363;
|
||||
--gray-light-12: #211e1e;
|
||||
--gray-dark-alpha-1: #82383803;
|
||||
--gray-dark-alpha-2: #e6c6c60b;
|
||||
--gray-dark-alpha-3: #edd5d516;
|
||||
--gray-dark-alpha-4: #f2e1e11e;
|
||||
--gray-dark-alpha-5: #f5e8e826;
|
||||
--gray-dark-alpha-6: #f5e8e831;
|
||||
--gray-dark-alpha-7: #f7ecec3f;
|
||||
--gray-dark-alpha-8: #faf5f559;
|
||||
--gray-dark-alpha-9: #faf5f467;
|
||||
--gray-dark-alpha-10: #fbf5f576;
|
||||
--gray-dark-alpha-11: #fcf9f9b2;
|
||||
--gray-dark-alpha-12: #fdfbfbf0;
|
||||
--gray-light-alpha-1: #55000003;
|
||||
--gray-light-alpha-2: #25000007;
|
||||
--gray-light-alpha-3: #1100000f;
|
||||
--gray-light-alpha-4: #0c000017;
|
||||
--gray-light-alpha-5: #1100001f;
|
||||
--gray-light-alpha-6: #07000026;
|
||||
--gray-light-alpha-7: #0b060032;
|
||||
--gray-light-alpha-8: #04000044;
|
||||
--gray-light-alpha-9: #07000074;
|
||||
--gray-light-alpha-10: #0400009c;
|
||||
--gray-light-alpha-11: #0700007e;
|
||||
--gray-light-alpha-12: #020000df;
|
||||
--smoke-dark-1: #131010;
|
||||
--smoke-dark-2: #1b1818;
|
||||
--smoke-dark-3: #252121;
|
||||
@@ -589,4 +685,88 @@
|
||||
--amber-dark-alpha-10: #ffce48f9;
|
||||
--amber-dark-alpha-11: #ffab0eef;
|
||||
--amber-dark-alpha-12: #fff8e1f9;
|
||||
|
||||
/* Legacy palette aliases (keeps older themes working) */
|
||||
--smoke-light-1: var(--gray-light-1);
|
||||
--smoke-light-2: var(--gray-light-2);
|
||||
--smoke-light-3: var(--gray-light-3);
|
||||
--smoke-light-4: var(--gray-light-4);
|
||||
--smoke-light-5: var(--gray-light-5);
|
||||
--smoke-light-6: var(--gray-light-6);
|
||||
--smoke-light-7: var(--gray-light-7);
|
||||
--smoke-light-8: var(--gray-light-8);
|
||||
--smoke-light-9: var(--gray-light-9);
|
||||
--smoke-light-10: var(--gray-light-10);
|
||||
--smoke-light-11: var(--gray-light-11);
|
||||
--smoke-light-12: var(--gray-light-12);
|
||||
|
||||
--smoke-dark-1: var(--gray-dark-1);
|
||||
--smoke-dark-2: var(--gray-dark-2);
|
||||
--smoke-dark-3: var(--gray-dark-3);
|
||||
--smoke-dark-4: var(--gray-dark-4);
|
||||
--smoke-dark-5: var(--gray-dark-5);
|
||||
--smoke-dark-6: var(--gray-dark-6);
|
||||
--smoke-dark-7: var(--gray-dark-7);
|
||||
--smoke-dark-8: var(--gray-dark-8);
|
||||
--smoke-dark-9: var(--gray-dark-9);
|
||||
--smoke-dark-10: var(--gray-dark-10);
|
||||
--smoke-dark-11: var(--gray-dark-11);
|
||||
--smoke-dark-12: var(--gray-dark-12);
|
||||
|
||||
--smoke-light-alpha-1: var(--gray-light-alpha-1);
|
||||
--smoke-light-alpha-2: var(--gray-light-alpha-2);
|
||||
--smoke-light-alpha-3: var(--gray-light-alpha-3);
|
||||
--smoke-light-alpha-4: var(--gray-light-alpha-4);
|
||||
--smoke-light-alpha-5: var(--gray-light-alpha-5);
|
||||
--smoke-light-alpha-6: var(--gray-light-alpha-6);
|
||||
--smoke-light-alpha-7: var(--gray-light-alpha-7);
|
||||
--smoke-light-alpha-8: var(--gray-light-alpha-8);
|
||||
--smoke-light-alpha-9: var(--gray-light-alpha-9);
|
||||
--smoke-light-alpha-10: var(--gray-light-alpha-10);
|
||||
--smoke-light-alpha-11: var(--gray-light-alpha-11);
|
||||
--smoke-light-alpha-12: var(--gray-light-alpha-12);
|
||||
|
||||
--smoke-dark-alpha-1: var(--gray-dark-alpha-1);
|
||||
--smoke-dark-alpha-2: var(--gray-dark-alpha-2);
|
||||
--smoke-dark-alpha-3: var(--gray-dark-alpha-3);
|
||||
--smoke-dark-alpha-4: var(--gray-dark-alpha-4);
|
||||
--smoke-dark-alpha-5: var(--gray-dark-alpha-5);
|
||||
--smoke-dark-alpha-6: var(--gray-dark-alpha-6);
|
||||
--smoke-dark-alpha-7: var(--gray-dark-alpha-7);
|
||||
--smoke-dark-alpha-8: var(--gray-dark-alpha-8);
|
||||
--smoke-dark-alpha-9: var(--gray-dark-alpha-9);
|
||||
--smoke-dark-alpha-10: var(--gray-dark-alpha-10);
|
||||
--smoke-dark-alpha-11: var(--gray-dark-alpha-11);
|
||||
--smoke-dark-alpha-12: var(--gray-dark-alpha-12);
|
||||
|
||||
--amber-lightalpha-1: var(--amber-light-alpha-1);
|
||||
--amber-lightalpha-2: var(--amber-light-alpha-2);
|
||||
--amber-lightalpha-3: var(--amber-light-alpha-3);
|
||||
--amber-lightalpha-4: var(--amber-light-alpha-4);
|
||||
--amber-lightalpha-5: var(--amber-light-alpha-5);
|
||||
--amber-lightalpha-6: var(--amber-light-alpha-6);
|
||||
--amber-lightalpha-7: var(--amber-light-alpha-7);
|
||||
--amber-lightalpha-8: var(--amber-light-alpha-8);
|
||||
--amber-lightalpha-9: var(--amber-light-alpha-9);
|
||||
--amber-lightalpha-10: var(--amber-light-alpha-10);
|
||||
--amber-lightalpha-11: var(--amber-light-alpha-11);
|
||||
--amber-lightalpha-12: var(--amber-light-alpha-12);
|
||||
|
||||
--amber-darkalpha-1: var(--amber-dark-alpha-1);
|
||||
--amber-darkalpha-2: var(--amber-dark-alpha-2);
|
||||
--amber-darkalpha-3: var(--amber-dark-alpha-3);
|
||||
--amber-darkalpha-4: var(--amber-dark-alpha-4);
|
||||
--amber-darkalpha-5: var(--amber-dark-alpha-5);
|
||||
--amber-darkalpha-6: var(--amber-dark-alpha-6);
|
||||
--amber-darkalpha-7: var(--amber-dark-alpha-7);
|
||||
--amber-darkalpha-8: var(--amber-dark-alpha-8);
|
||||
--amber-darkalpha-9: var(--amber-dark-alpha-9);
|
||||
--amber-darkalpha-10: var(--amber-dark-alpha-10);
|
||||
--amber-darkalpha-11: var(--amber-dark-alpha-11);
|
||||
--amber-darkalpha-12: var(--amber-dark-alpha-12);
|
||||
|
||||
--purple-light-9: var(--lilac-light-9);
|
||||
--purple-dark-9: var(--lilac-dark-9);
|
||||
--cyan-light-9: var(--blue-light-9);
|
||||
--cyan-dark-9: var(--blue-dark-9);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { DesktopTheme } from "./types"
|
||||
import oc1ThemeJson from "./themes/oc-1.json"
|
||||
import oc2ThemeJson from "./themes/oc-2.json"
|
||||
import tokyoThemeJson from "./themes/tokyonight.json"
|
||||
import draculaThemeJson from "./themes/dracula.json"
|
||||
import monokaiThemeJson from "./themes/monokai.json"
|
||||
@@ -16,6 +17,7 @@ import gruvboxThemeJson from "./themes/gruvbox.json"
|
||||
import auraThemeJson from "./themes/aura.json"
|
||||
|
||||
export const oc1Theme = oc1ThemeJson as DesktopTheme
|
||||
export const oc2Theme = oc2ThemeJson as DesktopTheme
|
||||
export const tokyonightTheme = tokyoThemeJson as DesktopTheme
|
||||
export const draculaTheme = draculaThemeJson as DesktopTheme
|
||||
export const monokaiTheme = monokaiThemeJson as DesktopTheme
|
||||
@@ -33,6 +35,7 @@ export const auraTheme = auraThemeJson as DesktopTheme
|
||||
|
||||
export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
|
||||
"oc-1": oc1Theme,
|
||||
"oc-2": oc2Theme,
|
||||
aura: auraTheme,
|
||||
ayu: ayuTheme,
|
||||
carbonfox: carbonfoxTheme,
|
||||
|
||||
@@ -32,6 +32,7 @@ export { ThemeProvider, useTheme, type ColorScheme } from "./context"
|
||||
export {
|
||||
DEFAULT_THEMES,
|
||||
oc1Theme,
|
||||
oc2Theme,
|
||||
tokyonightTheme,
|
||||
draculaTheme,
|
||||
monokaiTheme,
|
||||
|
||||
532
packages/ui/src/theme/themes/oc-2.json
Normal file
532
packages/ui/src/theme/themes/oc-2.json
Normal file
@@ -0,0 +1,532 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
||||
"name": "OC-2",
|
||||
"id": "oc-2",
|
||||
"light": {
|
||||
"seeds": {
|
||||
"neutral": "#8e8b8b",
|
||||
"primary": "#dcde8d",
|
||||
"success": "#12c905",
|
||||
"warning": "#ffdc17",
|
||||
"error": "#fc533a",
|
||||
"info": "#a753ae",
|
||||
"interactive": "#034cff",
|
||||
"diffAdd": "#9ff29a",
|
||||
"diffDelete": "#fc533a"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#f8f7f7",
|
||||
"background-weak": "var(--gray-light-3)",
|
||||
"background-strong": "var(--gray-light-1)",
|
||||
"background-stronger": "#fcfcfc",
|
||||
"surface-base": "var(--gray-light-alpha-2)",
|
||||
"base": "var(--gray-light-alpha-2)",
|
||||
"surface-base-hover": "#0500000f",
|
||||
"surface-base-active": "var(--gray-light-alpha-3)",
|
||||
"surface-base-interactive-active": "var(--cobalt-light-alpha-3)",
|
||||
"base2": "var(--gray-light-alpha-2)",
|
||||
"base3": "var(--gray-light-alpha-2)",
|
||||
"surface-inset-base": "var(--gray-light-alpha-2)",
|
||||
"surface-inset-base-hover": "var(--gray-light-alpha-3)",
|
||||
"surface-inset-strong": "#1f000017",
|
||||
"surface-inset-strong-hover": "#1f000017",
|
||||
"surface-raised-base": "var(--gray-light-alpha-2)",
|
||||
"surface-float-base": "var(--gray-dark-1)",
|
||||
"surface-float-base-hover": "var(--gray-dark-2)",
|
||||
"surface-raised-base-hover": "var(--gray-light-alpha-3)",
|
||||
"surface-raised-base-active": "var(--gray-light-alpha-5)",
|
||||
"surface-raised-strong": "var(--gray-light-1)",
|
||||
"surface-raised-strong-hover": "var(--white)",
|
||||
"surface-raised-stronger": "var(--white)",
|
||||
"surface-raised-stronger-hover": "var(--white)",
|
||||
"surface-weak": "var(--gray-light-alpha-3)",
|
||||
"surface-weaker": "var(--gray-light-alpha-4)",
|
||||
"surface-strong": "#ffffff",
|
||||
"surface-raised-stronger-non-alpha": "var(--white)",
|
||||
"surface-brand-base": "var(--yuzu-light-9)",
|
||||
"surface-brand-hover": "var(--yuzu-light-10)",
|
||||
"surface-interactive-base": "var(--cobalt-light-3)",
|
||||
"surface-interactive-hover": "#E5F0FF",
|
||||
"surface-interactive-weak": "var(--cobalt-light-2)",
|
||||
"surface-interactive-weak-hover": "var(--cobalt-light-3)",
|
||||
"surface-success-base": "var(--apple-light-3)",
|
||||
"surface-success-weak": "var(--apple-light-2)",
|
||||
"surface-success-strong": "var(--apple-light-9)",
|
||||
"surface-warning-base": "var(--solaris-light-3)",
|
||||
"surface-warning-weak": "var(--solaris-light-2)",
|
||||
"surface-warning-strong": "var(--solaris-light-9)",
|
||||
"surface-critical-base": "var(--ember-light-3)",
|
||||
"surface-critical-weak": "var(--ember-light-2)",
|
||||
"surface-critical-strong": "var(--ember-light-9)",
|
||||
"surface-info-base": "var(--lilac-light-3)",
|
||||
"surface-info-weak": "var(--lilac-light-2)",
|
||||
"surface-info-strong": "var(--lilac-light-9)",
|
||||
"surface-diff-unchanged-base": "#ffffff00",
|
||||
"surface-diff-skip-base": "var(--gray-light-2)",
|
||||
"surface-diff-hidden-base": "var(--blue-light-3)",
|
||||
"surface-diff-hidden-weak": "var(--blue-light-2)",
|
||||
"surface-diff-hidden-weaker": "var(--blue-light-1)",
|
||||
"surface-diff-hidden-strong": "var(--blue-light-5)",
|
||||
"surface-diff-hidden-stronger": "var(--blue-light-9)",
|
||||
"surface-diff-add-base": "#dafbe0",
|
||||
"surface-diff-add-weak": "var(--mint-light-2)",
|
||||
"surface-diff-add-weaker": "var(--mint-light-1)",
|
||||
"surface-diff-add-strong": "var(--mint-light-5)",
|
||||
"surface-diff-add-stronger": "var(--mint-light-9)",
|
||||
"surface-diff-delete-base": "var(--ember-light-3)",
|
||||
"surface-diff-delete-weak": "var(--ember-light-2)",
|
||||
"surface-diff-delete-weaker": "var(--ember-light-1)",
|
||||
"surface-diff-delete-strong": "var(--ember-light-6)",
|
||||
"surface-diff-delete-stronger": "var(--ember-light-9)",
|
||||
"input-base": "var(--gray-light-1)",
|
||||
"input-hover": "var(--gray-light-2)",
|
||||
"input-active": "var(--cobalt-light-1)",
|
||||
"input-selected": "var(--cobalt-light-4)",
|
||||
"input-focus": "var(--cobalt-light-1)",
|
||||
"input-disabled": "var(--gray-light-4)",
|
||||
"text-base": "var(--gray-light-11)",
|
||||
"text-weak": "var(--gray-light-9)",
|
||||
"text-weaker": "var(--gray-light-8)",
|
||||
"text-strong": "var(--gray-light-12)",
|
||||
"text-invert-base": "var(--gray-dark-alpha-11)",
|
||||
"text-invert-weak": "var(--gray-dark-alpha-9)",
|
||||
"text-invert-weaker": "var(--gray-dark-alpha-8)",
|
||||
"text-invert-strong": "var(--gray-dark-alpha-12)",
|
||||
"text-interactive-base": "var(--cobalt-light-9)",
|
||||
"text-on-brand-base": "var(--gray-light-alpha-11)",
|
||||
"text-on-interactive-base": "var(--gray-light-1)",
|
||||
"text-on-interactive-weak": "var(--gray-dark-alpha-11)",
|
||||
"text-on-success-base": "var(--apple-light-10)",
|
||||
"text-on-critical-base": "var(--ember-light-10)",
|
||||
"text-on-critical-weak": "var(--ember-light-8)",
|
||||
"text-on-critical-strong": "var(--ember-light-12)",
|
||||
"text-on-warning-base": "var(--gray-dark-alpha-11)",
|
||||
"text-on-info-base": "var(--gray-dark-alpha-11)",
|
||||
"text-diff-add-base": "var(--mint-light-11)",
|
||||
"text-diff-delete-base": "var(--ember-light-10)",
|
||||
"text-diff-delete-strong": "var(--ember-light-12)",
|
||||
"text-diff-add-strong": "var(--mint-light-12)",
|
||||
"text-on-info-weak": "var(--gray-dark-alpha-9)",
|
||||
"text-on-info-strong": "var(--gray-dark-alpha-12)",
|
||||
"text-on-warning-weak": "var(--gray-dark-alpha-9)",
|
||||
"text-on-warning-strong": "var(--gray-dark-alpha-12)",
|
||||
"text-on-success-weak": "var(--apple-light-6)",
|
||||
"text-on-success-strong": "var(--apple-light-12)",
|
||||
"text-on-brand-weak": "var(--gray-light-alpha-9)",
|
||||
"text-on-brand-weaker": "var(--gray-light-alpha-8)",
|
||||
"text-on-brand-strong": "var(--gray-light-alpha-12)",
|
||||
"button-primary-base": "var(--gray-light-12)",
|
||||
"button-secondary-base": "var(--gray-light-1)",
|
||||
"button-secondary-hover": "FFFFFF0A",
|
||||
"border-base": "var(--gray-light-alpha-7)",
|
||||
"border-hover": "var(--gray-light-alpha-8)",
|
||||
"border-active": "var(--gray-light-alpha-9)",
|
||||
"border-selected": "var(--cobalt-light-alpha-9)",
|
||||
"border-disabled": "var(--gray-light-alpha-8)",
|
||||
"border-focus": "var(--gray-light-alpha-9)",
|
||||
"border-weak-base": "var(--gray-light-alpha-5)",
|
||||
"border-strong-base": "var(--gray-light-alpha-7)",
|
||||
"border-strong-hover": "var(--gray-light-alpha-8)",
|
||||
"border-strong-active": "var(--gray-light-alpha-7)",
|
||||
"border-strong-selected": "var(--cobalt-light-alpha-6)",
|
||||
"border-strong-disabled": "var(--gray-light-alpha-6)",
|
||||
"border-strong-focus": "var(--gray-light-alpha-7)",
|
||||
"border-weak-hover": "var(--gray-light-alpha-6)",
|
||||
"border-weak-active": "var(--gray-light-alpha-7)",
|
||||
"border-weak-selected": "var(--cobalt-light-alpha-5)",
|
||||
"border-weak-disabled": "var(--gray-light-alpha-6)",
|
||||
"border-weak-focus": "var(--gray-light-alpha-7)",
|
||||
"border-interactive-base": "var(--cobalt-light-7)",
|
||||
"border-interactive-hover": "var(--cobalt-light-8)",
|
||||
"border-interactive-active": "var(--cobalt-light-9)",
|
||||
"border-interactive-selected": "var(--cobalt-light-9)",
|
||||
"border-interactive-disabled": "var(--gray-light-8)",
|
||||
"border-interactive-focus": "var(--cobalt-light-9)",
|
||||
"border-success-base": "var(--apple-light-6)",
|
||||
"border-success-hover": "var(--apple-light-7)",
|
||||
"border-success-selected": "var(--apple-light-9)",
|
||||
"border-warning-base": "var(--solaris-light-6)",
|
||||
"border-warning-hover": "var(--solaris-light-7)",
|
||||
"border-warning-selected": "var(--solaris-light-9)",
|
||||
"border-critical-base": "var(--ember-light-6)",
|
||||
"border-critical-hover": "var(--ember-light-7)",
|
||||
"border-critical-selected": "var(--ember-light-9)",
|
||||
"border-info-base": "var(--lilac-light-6)",
|
||||
"border-info-hover": "var(--lilac-light-7)",
|
||||
"border-info-selected": "var(--lilac-light-9)",
|
||||
"icon-base": "var(--gray-light-9)",
|
||||
"icon-hover": "var(--gray-light-11)",
|
||||
"icon-active": "var(--gray-light-12)",
|
||||
"icon-selected": "var(--gray-light-12)",
|
||||
"icon-disabled": "var(--gray-light-8)",
|
||||
"icon-focus": "var(--gray-light-12)",
|
||||
"icon-invert-base": "#ffffff",
|
||||
"icon-weak-base": "var(--gray-light-7)",
|
||||
"icon-weak-hover": "var(--gray-light-8)",
|
||||
"icon-weak-active": "var(--gray-light-9)",
|
||||
"icon-weak-selected": "var(--gray-light-10)",
|
||||
"icon-weak-disabled": "var(--gray-light-6)",
|
||||
"icon-weak-focus": "var(--gray-light-9)",
|
||||
"icon-strong-base": "var(--gray-light-12)",
|
||||
"icon-strong-hover": "#151313",
|
||||
"icon-strong-active": "#020202",
|
||||
"icon-strong-selected": "#020202",
|
||||
"icon-strong-disabled": "var(--gray-light-6)",
|
||||
"icon-strong-focus": "#020202",
|
||||
"icon-brand-base": "var(--gray-light-12)",
|
||||
"icon-interactive-base": "var(--cobalt-light-9)",
|
||||
"icon-success-base": "var(--apple-light-7)",
|
||||
"icon-success-hover": "var(--apple-light-8)",
|
||||
"icon-success-active": "var(--apple-light-11)",
|
||||
"icon-warning-base": "var(--amber-light-7)",
|
||||
"icon-warning-hover": "var(--amber-light-8)",
|
||||
"icon-warning-active": "var(--amber-light-11)",
|
||||
"icon-critical-base": "var(--ember-light-10)",
|
||||
"icon-critical-hover": "var(--ember-light-11)",
|
||||
"icon-critical-active": "var(--ember-light-12)",
|
||||
"icon-info-base": "var(--lilac-light-7)",
|
||||
"icon-info-hover": "var(--lilac-light-8)",
|
||||
"icon-info-active": "var(--lilac-light-11)",
|
||||
"icon-on-brand-base": "var(--gray-light-alpha-11)",
|
||||
"icon-on-brand-hover": "var(--gray-light-alpha-12)",
|
||||
"icon-on-brand-selected": "var(--gray-light-alpha-12)",
|
||||
"icon-on-interactive-base": "var(--gray-light-1)",
|
||||
"icon-agent-plan-base": "var(--purple-light-9)",
|
||||
"icon-agent-docs-base": "var(--amber-light-9)",
|
||||
"icon-agent-ask-base": "var(--cyan-light-9)",
|
||||
"icon-agent-build-base": "var(--cobalt-light-9)",
|
||||
"icon-on-success-base": "var(--apple-light-alpha-9)",
|
||||
"icon-on-success-hover": "var(--apple-light-alpha-10)",
|
||||
"icon-on-success-selected": "var(--apple-light-alpha-11)",
|
||||
"icon-on-warning-base": "var(--amber-lightalpha-9)",
|
||||
"icon-on-warning-hover": "var(--amber-lightalpha-10)",
|
||||
"icon-on-warning-selected": "var(--amber-lightalpha-11)",
|
||||
"icon-on-critical-base": "var(--ember-light-alpha-9)",
|
||||
"icon-on-critical-hover": "var(--ember-light-alpha-10)",
|
||||
"icon-on-critical-selected": "var(--ember-light-alpha-11)",
|
||||
"icon-on-info-base": "var(--lilac-light-9)",
|
||||
"icon-on-info-hover": "var(--lilac-light-alpha-10)",
|
||||
"icon-on-info-selected": "var(--lilac-light-alpha-11)",
|
||||
"icon-diff-add-base": "var(--mint-light-11)",
|
||||
"icon-diff-add-hover": "var(--mint-light-12)",
|
||||
"icon-diff-add-active": "var(--mint-light-12)",
|
||||
"icon-diff-delete-base": "var(--ember-light-10)",
|
||||
"icon-diff-delete-hover": "var(--ember-light-11)",
|
||||
"syntax-comment": "var(--text-weak)",
|
||||
"syntax-regexp": "var(--text-base)",
|
||||
"syntax-string": "#006656",
|
||||
"syntax-keyword": "var(--text-weak)",
|
||||
"syntax-primitive": "#fb4804",
|
||||
"syntax-operator": "var(--text-base)",
|
||||
"syntax-variable": "var(--text-strong)",
|
||||
"syntax-property": "#ed6dc8",
|
||||
"syntax-type": "#596600",
|
||||
"syntax-constant": "#007b80",
|
||||
"syntax-punctuation": "var(--text-base)",
|
||||
"syntax-object": "var(--text-strong)",
|
||||
"syntax-success": "var(--apple-light-10)",
|
||||
"syntax-warning": "var(--amber-light-10)",
|
||||
"syntax-critical": "var(--ember-light-10)",
|
||||
"syntax-info": "#0092a8",
|
||||
"syntax-diff-add": "var(--mint-light-11)",
|
||||
"syntax-diff-delete": "var(--ember-light-11)",
|
||||
"syntax-diff-unknown": "#ff0000",
|
||||
"markdown-heading": "#d68c27",
|
||||
"markdown-text": "#1a1a1a",
|
||||
"markdown-link": "#3b7dd8",
|
||||
"markdown-link-text": "#318795",
|
||||
"markdown-code": "#3d9a57",
|
||||
"markdown-block-quote": "#b0851f",
|
||||
"markdown-emph": "#b0851f",
|
||||
"markdown-strong": "#d68c27",
|
||||
"markdown-horizontal-rule": "#8a8a8a",
|
||||
"markdown-list-item": "#3b7dd8",
|
||||
"markdown-list-enumeration": "#318795",
|
||||
"markdown-image": "#3b7dd8",
|
||||
"markdown-image-text": "#318795",
|
||||
"markdown-code-block": "#1a1a1a",
|
||||
"border-color": "#ffffff",
|
||||
"border-weaker-base": "var(--gray-light-alpha-3)",
|
||||
"border-weaker-hover": "var(--gray-light-alpha-4)",
|
||||
"border-weaker-active": "var(--gray-light-alpha-6)",
|
||||
"border-weaker-selected": "var(--cobalt-light-alpha-4)",
|
||||
"border-weaker-disabled": "var(--gray-light-alpha-2)",
|
||||
"border-weaker-focus": "var(--gray-light-alpha-6)",
|
||||
"button-ghost-hover": "var(--gray-light-alpha-2)",
|
||||
"button-ghost-hover2": "var(--gray-light-alpha-3)",
|
||||
"avatar-background-pink": "#feeef8",
|
||||
"avatar-background-mint": "#e1fbf4",
|
||||
"avatar-background-orange": "#fff1e7",
|
||||
"avatar-background-purple": "#f9f1fe",
|
||||
"avatar-background-cyan": "#e7f9fb",
|
||||
"avatar-background-lime": "#eefadc",
|
||||
"avatar-text-pink": "#cd1d8d",
|
||||
"avatar-text-mint": "#147d6f",
|
||||
"avatar-text-orange": "#ed5f00",
|
||||
"avatar-text-purple": "#8445bc",
|
||||
"avatar-text-cyan": "#0894b3",
|
||||
"avatar-text-lime": "#5d770d"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"seeds": {
|
||||
"neutral": "#716c6b",
|
||||
"primary": "#fab283",
|
||||
"success": "#12c905",
|
||||
"warning": "#fcd53a",
|
||||
"error": "#fc533a",
|
||||
"info": "#edb2f1",
|
||||
"interactive": "#034cff",
|
||||
"diffAdd": "#c8ffc4",
|
||||
"diffDelete": "#fc533a"
|
||||
},
|
||||
"overrides": {
|
||||
"base": "var(--gray-dark-alpha-2)",
|
||||
"base2": "var(--gray-dark-alpha-2)",
|
||||
"base3": "var(--gray-dark-alpha-2)",
|
||||
"background-base": "#101010",
|
||||
"background-weak": "#1E1E1E",
|
||||
"background-strong": "#121212",
|
||||
"background-stronger": "#151515",
|
||||
"surface-base": "var(--gray-dark-alpha-2)",
|
||||
"surface-base-hover": "#FFFFFF0A",
|
||||
"surface-base-active": "var(--gray-dark-alpha-3)",
|
||||
"surface-base-interactive-active": "var(--cobalt-dark-alpha-2)",
|
||||
"surface-inset-base": "#0e0b0b7f",
|
||||
"surface-inset-base-hover": "#0e0b0b7f",
|
||||
"surface-inset-strong": "#060505cc",
|
||||
"surface-inset-strong-hover": "#060505cc",
|
||||
"surface-raised-base": "var(--gray-dark-alpha-3)",
|
||||
"surface-float-base": "var(--gray-dark-1)",
|
||||
"surface-float-base-hover": "var(--gray-dark-2)",
|
||||
"surface-raised-base-hover": "var(--gray-dark-alpha-4)",
|
||||
"surface-raised-base-active": "var(--gray-dark-alpha-5)",
|
||||
"surface-raised-strong": "var(--gray-dark-alpha-4)",
|
||||
"surface-raised-strong-hover": "var(--gray-dark-alpha-6)",
|
||||
"surface-raised-stronger": "var(--gray-dark-alpha-6)",
|
||||
"surface-raised-stronger-hover": "var(--gray-dark-alpha-7)",
|
||||
"surface-weak": "var(--gray-dark-alpha-4)",
|
||||
"surface-weaker": "var(--gray-dark-alpha-5)",
|
||||
"surface-strong": "var(--gray-dark-alpha-7)",
|
||||
"surface-raised-stronger-non-alpha": "#1B1B1B",
|
||||
"surface-brand-base": "var(--yuzu-light-9)",
|
||||
"surface-brand-hover": "var(--yuzu-light-10)",
|
||||
"surface-interactive-base": "var(--cobalt-dark-3)",
|
||||
"surface-interactive-hover": "#0A1D4D",
|
||||
"surface-interactive-weak": "var(--cobalt-dark-2)",
|
||||
"surface-interactive-weak-hover": "var(--cobalt-light-3)",
|
||||
"surface-success-base": "var(--apple-dark-3)",
|
||||
"surface-success-weak": "var(--apple-dark-2)",
|
||||
"surface-success-strong": "var(--apple-dark-9)",
|
||||
"surface-warning-base": "var(--solaris-light-3)",
|
||||
"surface-warning-weak": "var(--solaris-light-2)",
|
||||
"surface-warning-strong": "var(--solaris-light-9)",
|
||||
"surface-critical-base": "var(--ember-dark-3)",
|
||||
"surface-critical-weak": "var(--ember-dark-2)",
|
||||
"surface-critical-strong": "var(--ember-dark-9)",
|
||||
"surface-info-base": "var(--lilac-light-3)",
|
||||
"surface-info-weak": "var(--lilac-light-2)",
|
||||
"surface-info-strong": "var(--lilac-light-9)",
|
||||
"surface-diff-unchanged-base": "var(--gray-dark-1)",
|
||||
"surface-diff-skip-base": "var(--gray-dark-alpha-1)",
|
||||
"surface-diff-hidden-base": "var(--blue-dark-2)",
|
||||
"surface-diff-hidden-weak": "var(--blue-dark-1)",
|
||||
"surface-diff-hidden-weaker": "var(--blue-dark-3)",
|
||||
"surface-diff-hidden-strong": "var(--blue-dark-5)",
|
||||
"surface-diff-hidden-stronger": "var(--blue-dark-11)",
|
||||
"surface-diff-add-base": "var(--mint-dark-3)",
|
||||
"surface-diff-add-weak": "var(--mint-dark-4)",
|
||||
"surface-diff-add-weaker": "var(--mint-dark-3)",
|
||||
"surface-diff-add-strong": "var(--mint-dark-5)",
|
||||
"surface-diff-add-stronger": "var(--mint-dark-11)",
|
||||
"surface-diff-delete-base": "var(--ember-dark-3)",
|
||||
"surface-diff-delete-weak": "var(--ember-dark-4)",
|
||||
"surface-diff-delete-weaker": "var(--ember-dark-3)",
|
||||
"surface-diff-delete-strong": "var(--ember-dark-5)",
|
||||
"surface-diff-delete-stronger": "var(--ember-dark-11)",
|
||||
"input-base": "var(--gray-dark-2)",
|
||||
"input-hover": "var(--gray-dark-2)",
|
||||
"input-active": "var(--cobalt-dark-1)",
|
||||
"input-selected": "var(--cobalt-dark-2)",
|
||||
"input-focus": "var(--cobalt-dark-1)",
|
||||
"input-disabled": "var(--gray-dark-4)",
|
||||
"text-base": "var(--gray-dark-alpha-11)",
|
||||
"text-weak": "var(--gray-dark-alpha-9)",
|
||||
"text-weaker": "var(--gray-dark-alpha-8)",
|
||||
"text-strong": "var(--gray-dark-alpha-12)",
|
||||
"text-invert-base": "var(--gray-dark-alpha-11)",
|
||||
"text-invert-weak": "var(--gray-dark-alpha-9)",
|
||||
"text-invert-weaker": "var(--gray-dark-alpha-8)",
|
||||
"text-invert-strong": "var(--gray-dark-alpha-12)",
|
||||
"text-interactive-base": "var(--cobalt-dark-11)",
|
||||
"text-on-brand-base": "var(--gray-dark-alpha-11)",
|
||||
"text-on-interactive-base": "var(--gray-dark-12)",
|
||||
"text-on-interactive-weak": "var(--gray-dark-alpha-11)",
|
||||
"text-on-success-base": "var(--apple-dark-9)",
|
||||
"text-on-critical-base": "var(--ember-dark-9)",
|
||||
"text-on-critical-weak": "var(--ember-dark-8)",
|
||||
"text-on-critical-strong": "var(--ember-dark-12)",
|
||||
"text-on-warning-base": "var(--gray-dark-alpha-11)",
|
||||
"text-on-info-base": "var(--gray-dark-alpha-11)",
|
||||
"text-diff-add-base": "var(--mint-dark-11)",
|
||||
"text-diff-delete-base": "var(--ember-dark-9)",
|
||||
"text-diff-delete-strong": "var(--ember-dark-12)",
|
||||
"text-diff-add-strong": "var(--mint-dark-8)",
|
||||
"text-on-info-weak": "var(--gray-dark-alpha-9)",
|
||||
"text-on-info-strong": "var(--gray-dark-alpha-12)",
|
||||
"text-on-warning-weak": "var(--gray-dark-alpha-9)",
|
||||
"text-on-warning-strong": "var(--gray-dark-alpha-12)",
|
||||
"text-on-success-weak": "var(--apple-dark-8)",
|
||||
"text-on-success-strong": "var(--apple-dark-12)",
|
||||
"text-on-brand-weak": "var(--gray-dark-alpha-9)",
|
||||
"text-on-brand-weaker": "var(--gray-dark-alpha-8)",
|
||||
"text-on-brand-strong": "var(--gray-dark-alpha-12)",
|
||||
"button-primary-base": "var(--gray-dark-12)",
|
||||
"button-secondary-base": "var(--gray-dark-2)",
|
||||
"button-secondary-hover": "#FFFFFF0A",
|
||||
"border-base": "var(--gray-dark-alpha-7)",
|
||||
"border-hover": "var(--gray-dark-alpha-8)",
|
||||
"border-active": "var(--gray-dark-alpha-9)",
|
||||
"border-selected": "var(--cobalt-dark-alpha-11)",
|
||||
"border-disabled": "var(--gray-dark-alpha-8)",
|
||||
"border-focus": "var(--gray-dark-alpha-9)",
|
||||
"border-weak-base": "var(--gray-dark-alpha-5)",
|
||||
"border-weak-hover": "var(--gray-dark-alpha-7)",
|
||||
"border-weak-active": "var(--gray-dark-alpha-8)",
|
||||
"border-weak-selected": "var(--cobalt-dark-alpha-6)",
|
||||
"border-weak-disabled": "var(--gray-dark-alpha-6)",
|
||||
"border-weak-focus": "var(--gray-dark-alpha-8)",
|
||||
"border-strong-base": "var(--gray-dark-alpha-8)",
|
||||
"border-interactive-base": "var(--cobalt-light-7)",
|
||||
"border-interactive-hover": "var(--cobalt-light-8)",
|
||||
"border-interactive-active": "var(--cobalt-light-9)",
|
||||
"border-interactive-selected": "var(--cobalt-light-9)",
|
||||
"border-interactive-disabled": "var(--gray-light-8)",
|
||||
"border-interactive-focus": "var(--cobalt-light-9)",
|
||||
"border-success-base": "var(--apple-light-6)",
|
||||
"border-success-hover": "var(--apple-light-7)",
|
||||
"border-success-selected": "var(--apple-light-9)",
|
||||
"border-warning-base": "var(--solaris-light-6)",
|
||||
"border-warning-hover": "var(--solaris-light-7)",
|
||||
"border-warning-selected": "var(--solaris-light-9)",
|
||||
"border-critical-base": "var(--ember-dark-5)",
|
||||
"border-critical-hover": "var(--ember-dark-7)",
|
||||
"border-critical-selected": "var(--ember-dark-9)",
|
||||
"border-info-base": "var(--lilac-light-6)",
|
||||
"border-info-hover": "var(--lilac-light-7)",
|
||||
"border-info-selected": "var(--lilac-light-9)",
|
||||
"icon-base": "var(--gray-dark-10)",
|
||||
"icon-hover": "var(--gray-dark-11)",
|
||||
"icon-active": "var(--gray-dark-12)",
|
||||
"icon-selected": "var(--gray-dark-12)",
|
||||
"icon-disabled": "var(--gray-dark-8)",
|
||||
"icon-focus": "var(--gray-dark-12)",
|
||||
"icon-invert-base": "var(--gray-dark-1)",
|
||||
"icon-weak-base": "var(--gray-dark-6)",
|
||||
"icon-weak-hover": "var(--gray-light-7)",
|
||||
"icon-weak-active": "var(--gray-light-8)",
|
||||
"icon-weak-selected": "var(--gray-light-9)",
|
||||
"icon-weak-disabled": "var(--gray-light-4)",
|
||||
"icon-weak-focus": "var(--gray-light-9)",
|
||||
"icon-strong-base": "var(--gray-dark-12)",
|
||||
"icon-strong-hover": "#F3F3F3",
|
||||
"icon-strong-active": "#EBEBEB",
|
||||
"icon-strong-selected": "#FCFCFC",
|
||||
"icon-strong-disabled": "var(--gray-dark-7)",
|
||||
"icon-strong-focus": "#FCFCFC",
|
||||
"icon-brand-base": "var(--white)",
|
||||
"icon-interactive-base": "var(--cobalt-dark-11)",
|
||||
"icon-success-base": "var(--apple-dark-9)",
|
||||
"icon-success-hover": "var(--apple-dark-10)",
|
||||
"icon-success-active": "var(--apple-dark-11)",
|
||||
"icon-warning-base": "var(--amber-dark-9)",
|
||||
"icon-warning-hover": "var(--amber-dark-8)",
|
||||
"icon-warning-active": "var(--amber-dark-11)",
|
||||
"icon-critical-base": "var(--ember-dark-9)",
|
||||
"icon-critical-hover": "var(--ember-dark-11)",
|
||||
"icon-critical-active": "var(--ember-dark-12)",
|
||||
"icon-info-base": "var(--lilac-dark-7)",
|
||||
"icon-info-hover": "var(--lilac-dark-8)",
|
||||
"icon-info-active": "var(--lilac-dark-11)",
|
||||
"icon-on-brand-base": "var(--gray-light-alpha-11)",
|
||||
"icon-on-brand-hover": "var(--gray-light-alpha-12)",
|
||||
"icon-on-brand-selected": "var(--gray-light-alpha-12)",
|
||||
"icon-on-interactive-base": "var(--gray-dark-12)",
|
||||
"icon-agent-plan-base": "var(--purple-dark-9)",
|
||||
"icon-agent-docs-base": "var(--amber-dark-9)",
|
||||
"icon-agent-ask-base": "var(--cyan-dark-9)",
|
||||
"icon-agent-build-base": "var(--cobalt-dark-11)",
|
||||
"icon-on-success-base": "var(--apple-dark-alpha-9)",
|
||||
"icon-on-success-hover": "var(--apple-dark-alpha-10)",
|
||||
"icon-on-success-selected": "var(--apple-dark-alpha-11)",
|
||||
"icon-on-warning-base": "var(--amber-darkalpha-9)",
|
||||
"icon-on-warning-hover": "var(--amber-darkalpha-10)",
|
||||
"icon-on-warning-selected": "var(--amber-darkalpha-11)",
|
||||
"icon-on-critical-base": "var(--ember-dark-alpha-9)",
|
||||
"icon-on-critical-hover": "var(--ember-dark-alpha-10)",
|
||||
"icon-on-critical-selected": "var(--ember-dark-alpha-11)",
|
||||
"icon-on-info-base": "var(--lilac-dark-9)",
|
||||
"icon-on-info-hover": "var(--lilac-dark-alpha-10)",
|
||||
"icon-on-info-selected": "var(--lilac-dark-alpha-11)",
|
||||
"icon-diff-add-base": "var(--mint-dark-11)",
|
||||
"icon-diff-add-hover": "var(--mint-dark-10)",
|
||||
"icon-diff-add-active": "var(--mint-dark-11)",
|
||||
"icon-diff-delete-base": "var(--ember-dark-9)",
|
||||
"icon-diff-delete-hover": "var(--ember-dark-10)",
|
||||
"syntax-comment": "var(--text-weak)",
|
||||
"syntax-regexp": "var(--text-base)",
|
||||
"syntax-string": "#00ceb9",
|
||||
"syntax-keyword": "var(--text-weak)",
|
||||
"syntax-primitive": "#ffba92",
|
||||
"syntax-operator": "var(--text-weak)",
|
||||
"syntax-variable": "var(--text-strong)",
|
||||
"syntax-property": "#ff9ae2",
|
||||
"syntax-type": "#ecf58c",
|
||||
"syntax-constant": "#93e9f6",
|
||||
"syntax-punctuation": "var(--text-weak)",
|
||||
"syntax-object": "var(--text-strong)",
|
||||
"syntax-success": "var(--apple-dark-10)",
|
||||
"syntax-warning": "var(--amber-dark-10)",
|
||||
"syntax-critical": "var(--ember-dark-10)",
|
||||
"syntax-info": "#93e9f6",
|
||||
"syntax-diff-add": "var(--mint-dark-11)",
|
||||
"syntax-diff-delete": "var(--ember-dark-11)",
|
||||
"syntax-diff-unknown": "#ff0000",
|
||||
"markdown-heading": "#9d7cd8",
|
||||
"markdown-text": "#eeeeee",
|
||||
"markdown-link": "#fab283",
|
||||
"markdown-link-text": "#56b6c2",
|
||||
"markdown-code": "#7fd88f",
|
||||
"markdown-block-quote": "#e5c07b",
|
||||
"markdown-emph": "#e5c07b",
|
||||
"markdown-strong": "#f5a742",
|
||||
"markdown-horizontal-rule": "#808080",
|
||||
"markdown-list-item": "#fab283",
|
||||
"markdown-list-enumeration": "#56b6c2",
|
||||
"markdown-image": "#fab283",
|
||||
"markdown-image-text": "#56b6c2",
|
||||
"markdown-code-block": "#eeeeee",
|
||||
"border-color": "#ffffff",
|
||||
"border-weaker-base": "var(--gray-dark-alpha-3)",
|
||||
"border-weaker-hover": "var(--gray-dark-alpha-4)",
|
||||
"border-weaker-active": "var(--gray-dark-alpha-6)",
|
||||
"border-weaker-selected": "var(--cobalt-dark-alpha-3)",
|
||||
"border-weaker-disabled": "var(--gray-dark-alpha-2)",
|
||||
"border-weaker-focus": "var(--gray-dark-alpha-6)",
|
||||
"button-ghost-hover": "var(--gray-dark-alpha-2)",
|
||||
"button-ghost-hover2": "var(--gray-dark-alpha-3)",
|
||||
"avatar-background-pink": "#501b3f",
|
||||
"avatar-background-mint": "#033a34",
|
||||
"avatar-background-orange": "#5f2a06",
|
||||
"avatar-background-purple": "#432155",
|
||||
"avatar-background-cyan": "#0f3058",
|
||||
"avatar-background-lime": "#2b3711",
|
||||
"avatar-text-pink": "#e34ba9",
|
||||
"avatar-text-mint": "#95f3d9",
|
||||
"avatar-text-orange": "#ff802b",
|
||||
"avatar-text-purple": "#9d5bd2",
|
||||
"avatar-text-cyan": "#369eff",
|
||||
"avatar-text-lime": "#c4f042"
|
||||
}
|
||||
}
|
||||
}
|
||||
79
script/duplicate-issue.ts
Normal file
79
script/duplicate-issue.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "bun"
|
||||
import { createOpencode } from "@opencode-ai/sdk"
|
||||
import { parseArgs } from "util"
|
||||
|
||||
async function main() {
|
||||
const { values, positionals } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
file: { type: "string", short: "f" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
allowPositionals: true,
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(`
|
||||
Usage: bun script/duplicate-issue.ts [options] <message>
|
||||
|
||||
Options:
|
||||
-f, --file <path> File to attach to the prompt
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
bun script/duplicate-issue.ts -f issue_info.txt "Check the attached file for issue details"
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const message = positionals.join(" ")
|
||||
if (!message) {
|
||||
console.error("Error: message is required")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const opencode = await createOpencode({ port: 0 })
|
||||
|
||||
try {
|
||||
const parts: Array<{ type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string }> =
|
||||
[]
|
||||
|
||||
if (values.file) {
|
||||
const resolved = path.resolve(process.cwd(), values.file)
|
||||
const file = Bun.file(resolved)
|
||||
if (!(await file.exists())) {
|
||||
console.error(`Error: file not found: ${values.file}`)
|
||||
process.exit(1)
|
||||
}
|
||||
parts.push({
|
||||
type: "file",
|
||||
url: pathToFileURL(resolved).href,
|
||||
filename: path.basename(resolved),
|
||||
mime: "text/plain",
|
||||
})
|
||||
}
|
||||
|
||||
parts.push({ type: "text", text: message })
|
||||
|
||||
const session = await opencode.client.session.create()
|
||||
const result = await opencode.client.session
|
||||
.prompt({
|
||||
path: { id: session.data!.id },
|
||||
body: {
|
||||
agent: "duplicate-issue",
|
||||
parts,
|
||||
},
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
})
|
||||
.then((x) => x.data?.parts?.find((y) => y.type === "text")?.text ?? "")
|
||||
|
||||
console.log(result.trim())
|
||||
} finally {
|
||||
opencode.server.close()
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user