Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
314f7c56e7 | ||
|
|
58ca434c78 | ||
|
|
70f14cccd6 | ||
|
|
86df4073d1 | ||
|
|
69117fa453 | ||
|
|
dc01071498 | ||
|
|
57b04d9eb7 | ||
|
|
07dbc30c63 | ||
|
|
1ae38c90a3 | ||
|
|
9609c1803e | ||
|
|
6e0e87fb2a | ||
|
|
c875d11959 | ||
|
|
08a83b7337 | ||
|
|
79a4e35a74 | ||
|
|
40ed73af17 | ||
|
|
74da6b1bef | ||
|
|
c35e1a03d1 | ||
|
|
92d4366a20 | ||
|
|
17a7c824b8 | ||
|
|
0befc5d602 | ||
|
|
8355ee2061 | ||
|
|
62fed8d2ce | ||
|
|
6fbe28619c | ||
|
|
156cc6cffe | ||
|
|
bcd1dddcbe | ||
|
|
6eaaaffcdd | ||
|
|
766fa521ea | ||
|
|
ecafa40bcf | ||
|
|
25f4721c71 | ||
|
|
a433766a31 | ||
|
|
c93d50e8c7 | ||
|
|
3f879859d7 | ||
|
|
ee62dc0745 | ||
|
|
b7aefa715a | ||
|
|
796bc390db | ||
|
|
703ae49675 | ||
|
|
37c0570a9f | ||
|
|
4dea0209bb | ||
|
|
bb4b24a05f | ||
|
|
e789abec79 | ||
|
|
118617473e | ||
|
|
a4beb60e19 | ||
|
|
3f0f910f7b | ||
|
|
5bf841ab7a | ||
|
|
49727e3eab | ||
|
|
592c6ef97f | ||
|
|
00579f0ec1 | ||
|
|
69d516c7fa | ||
|
|
bedeb626b2 | ||
|
|
a4c14dbb2d | ||
|
|
036b24791d | ||
|
|
93b71477e6 | ||
|
|
1357319f6f | ||
|
|
e729eed34d | ||
|
|
2e5fdd8cef | ||
|
|
21f15f15c1 | ||
|
|
c6344c5714 | ||
|
|
7505fa61b9 | ||
|
|
77bb5af092 | ||
|
|
0c4fe73cbf | ||
|
|
e132f6183d | ||
|
|
e06ebb6780 | ||
|
|
66d99ba527 | ||
|
|
f2021a85d6 | ||
|
|
7d54f893c9 | ||
|
|
e1f80c0067 | ||
|
|
4ff13d3290 | ||
|
|
832d8da453 | ||
|
|
b5d61b77f7 | ||
|
|
790e9947bd | ||
|
|
2056781cf7 | ||
|
|
ed5f76d849 | ||
|
|
93102dc84b | ||
|
|
e2920ac262 | ||
|
|
aa5e39e744 | ||
|
|
296cc41a07 | ||
|
|
482239b848 | ||
|
|
17b07877e5 | ||
|
|
dedaa34dc1 | ||
|
|
5785ded6e2 | ||
|
|
d1876e3031 | ||
|
|
9fa3e0a0ec | ||
|
|
47c327641b | ||
|
|
81583cddbd | ||
|
|
d16ae1fc4e | ||
|
|
14e00a06b6 | ||
|
|
5cc44c872e | ||
|
|
cadc5982f1 | ||
|
|
6aa157cfe6 | ||
|
|
9fff9a37d0 | ||
|
|
b289fd9dc7 | ||
|
|
13d4a802ac | ||
|
|
c4ae3e429c | ||
|
|
e9cb360cb7 | ||
|
|
596d4e4490 | ||
|
|
45451095f7 | ||
|
|
aae354c951 | ||
|
|
4cddda3e16 | ||
|
|
834aa036a4 | ||
|
|
6d1b6a6fb1 | ||
|
|
e78fe8dcba | ||
|
|
f62d826037 | ||
|
|
342f4239e4 | ||
|
|
5141fe8d7d | ||
|
|
3a9dd306db | ||
|
|
e32b6e2cc2 | ||
|
|
fab0e5de04 | ||
|
|
61105b487f | ||
|
|
6f584ec641 | ||
|
|
3a2b2f13f2 | ||
|
|
be13e71fb9 | ||
|
|
dd0c049119 | ||
|
|
ee2b57958d | ||
|
|
4178b3c6ae | ||
|
|
2c2752ee02 | ||
|
|
5a17f44da4 | ||
|
|
354e55ecef | ||
|
|
10735f93ca | ||
|
|
ccaebdcd16 | ||
|
|
2bbd7a167a | ||
|
|
f12d470b33 | ||
|
|
0835170224 | ||
|
|
3530885f48 | ||
|
|
a071a2b7f4 | ||
|
|
b2f2c9ac37 | ||
|
|
631722213b | ||
|
|
80b25c79bb | ||
|
|
3d9c5b4adf | ||
|
|
a3064e2c32 | ||
|
|
275bc4d2c8 | ||
|
|
39106718ed | ||
|
|
02cfdfbf5b | ||
|
|
1ec71e419b |
@@ -17,7 +17,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: oven-sh/setup-bun@v1
|
- uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.17
|
bun-version: 1.2.19
|
||||||
|
|
||||||
- run: bun install
|
- run: bun install
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: Duplicate Issue Detection
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-duplicates:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Install opencode
|
||||||
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
- name: Check for duplicate issues
|
||||||
|
env:
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
OPENCODE_PERMISSION: |
|
||||||
|
{
|
||||||
|
"bash": {
|
||||||
|
"gh issue*": "allow",
|
||||||
|
"*": "deny"
|
||||||
|
},
|
||||||
|
"webfetch": "deny"
|
||||||
|
}
|
||||||
|
run: |
|
||||||
|
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created: '${{ github.event.issue.title }}'
|
||||||
|
|
||||||
|
Issue number:
|
||||||
|
${{ github.event.issue.number }}
|
||||||
|
Issue body:
|
||||||
|
${{ github.event.issue.body }}
|
||||||
|
|
||||||
|
Please search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
|
||||||
|
Consider:
|
||||||
|
1. Similar titles or descriptions
|
||||||
|
2. Same error messages or symptoms
|
||||||
|
3. Related functionality or components
|
||||||
|
4. Similar feature requests
|
||||||
|
|
||||||
|
If you find any potential duplicates, please comment on the new issue with:
|
||||||
|
- A brief explanation of why it might be a duplicate
|
||||||
|
- Links to the potentially duplicate issues
|
||||||
|
- A suggestion to check those issues first
|
||||||
|
|
||||||
|
Use this format for the comment:
|
||||||
|
'This issue might be a duplicate of existing issues. Please check:
|
||||||
|
- #[issue_number]: [brief description of similarity]
|
||||||
|
|
||||||
|
Feel free to ignore if none of these address your specific case.'
|
||||||
|
|
||||||
|
If no clear duplicates are found, do not comment."
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Guidelines Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Disabled - uncomment to re-enable
|
||||||
|
# pull_request_target:
|
||||||
|
# types: [opened, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-guidelines:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Install opencode
|
||||||
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
- name: Check PR guidelines compliance
|
||||||
|
env:
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
|
||||||
|
run: |
|
||||||
|
opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
|
||||||
|
|
||||||
|
<pr-number>
|
||||||
|
${{ github.event.pull_request.number }}
|
||||||
|
</pr-number>
|
||||||
|
|
||||||
|
<pr-description>
|
||||||
|
${{ github.event.pull_request.body }}
|
||||||
|
</pr-description>
|
||||||
|
|
||||||
|
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
|
||||||
|
|
||||||
|
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
|
||||||
|
|
||||||
|
Command MUST be like this.
|
||||||
|
```
|
||||||
|
gh api \
|
||||||
|
--method POST \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
|
||||||
|
-f 'body=[summary of issue]' -f 'commit_id=${{ github.event.pull_request.head.sha }}' -f 'path=[path-to-file]' -F "line=[line]" -f 'side=RIGHT'
|
||||||
|
```
|
||||||
|
|
||||||
|
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.17
|
bun-version: 1.2.19
|
||||||
|
|
||||||
- run: git fetch --force --tags
|
- run: git fetch --force --tags
|
||||||
- run: bun install -g @vscode/vsce
|
- run: bun install -g @vscode/vsce
|
||||||
|
|||||||
@@ -52,16 +52,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y pacman-package-manager
|
sudo apt-get install -y pacman-package-manager
|
||||||
|
|
||||||
- name: Setup SSH for AUR
|
- name: Setup SSH for AUR
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
|
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
|
||||||
chmod 600 ~/.ssh/id_rsa
|
chmod 600 ~/.ssh/id_rsa
|
||||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
|
|
||||||
git config --global user.email "opencode@sst.dev"
|
git config --global user.email "opencode@sst.dev"
|
||||||
git config --global user.name "opencode"
|
git config --global user.name "opencode"
|
||||||
|
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
description: Use this agent when you are saked to commit and push code changes to a git repository.
|
description: Use this agent when you are asked to commit and push code changes to a git repository.
|
||||||
mode: subagent
|
mode: subagent
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,51 @@
|
|||||||
# Download Stats
|
# Download Stats
|
||||||
|
|
||||||
| Date | GitHub Downloads | npm Downloads | Total |
|
| Date | GitHub Downloads | npm Downloads | Total |
|
||||||
| ---------- | ---------------- | ---------------- | ---------------- |
|
| ---------- | ---------------- | ---------------- | ----------------- |
|
||||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||||
|
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||||
|
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||||
|
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||||
|
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"tree-sitter-bash": "0.23.3",
|
"tree-sitter-bash": "0.23.3",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"vscode-jsonrpc": "8.2.1",
|
"vscode-jsonrpc": "8.2.1",
|
||||||
|
"web-tree-sitter": "0.22.6",
|
||||||
"xdg-basedir": "5.1.0",
|
"xdg-basedir": "5.1.0",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -189,6 +190,9 @@
|
|||||||
"sharp",
|
"sharp",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
|
"tree-sitter",
|
||||||
|
"web-tree-sitter",
|
||||||
|
"tree-sitter-bash",
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
@@ -2481,6 +2485,8 @@
|
|||||||
|
|
||||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||||
|
|
||||||
|
"web-tree-sitter": ["web-tree-sitter@0.22.6", "", {}, "sha512-hS87TH71Zd6mGAmYCvlgxeGDjqd9GTeqXNqTT+u0Gs51uIozNIaaq/kUAbV/Zf56jb2ZOyG8BxZs2GG9wbLi6Q=="],
|
||||||
|
|
||||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode/cloud-core",
|
"name": "@opencode/cloud-core",
|
||||||
"version": "0.4.26",
|
"version": "0.5.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode/cloud-function",
|
"name": "@opencode/cloud-function",
|
||||||
"version": "0.4.26",
|
"version": "0.5.3",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode/cloud-web",
|
"name": "@opencode/cloud-web",
|
||||||
"version": "0.4.26",
|
"version": "0.5.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+6
-3
@@ -3,11 +3,11 @@
|
|||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.2.14",
|
"packageManager": "bun@1.2.19",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --conditions=development packages/opencode/src/index.ts",
|
"dev": "bun run --conditions=development packages/opencode/src/index.ts",
|
||||||
"typecheck": "bun run --filter='*' typecheck",
|
"typecheck": "bun run --filter='*' typecheck",
|
||||||
"stainless": "./scripts/stainless",
|
"generate": "(cd packages/sdk && ./js/script/generate.ts) && (cd packages/sdk/stainless && ./generate.ts)",
|
||||||
"postinstall": "./script/hooks"
|
"postinstall": "./script/hooks"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
@@ -46,7 +46,10 @@
|
|||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
"sharp"
|
"sharp",
|
||||||
|
"tree-sitter",
|
||||||
|
"tree-sitter-bash",
|
||||||
|
"web-tree-sitter"
|
||||||
],
|
],
|
||||||
"patchedDependencies": {}
|
"patchedDependencies": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode/function",
|
"name": "@opencode/function",
|
||||||
"version": "0.4.26",
|
"version": "0.5.3",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "0.4.26",
|
"version": "0.5.3",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
"tree-sitter": "0.22.4",
|
"tree-sitter": "0.22.4",
|
||||||
"tree-sitter-bash": "0.23.3",
|
"tree-sitter-bash": "0.23.3",
|
||||||
|
"web-tree-sitter": "0.22.6",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"vscode-jsonrpc": "8.2.1",
|
"vscode-jsonrpc": "8.2.1",
|
||||||
"xdg-basedir": "5.1.0",
|
"xdg-basedir": "5.1.0",
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ if (!snapshot) {
|
|||||||
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||||
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||||
|
|
||||||
// AUR package
|
/* AUR package - commented out as AUR is down
|
||||||
const pkgbuild = [
|
const pkgbuild = [
|
||||||
"# Maintainer: dax",
|
"# Maintainer: dax",
|
||||||
"# Maintainer: adam",
|
"# Maintainer: adam",
|
||||||
@@ -126,7 +126,7 @@ if (!snapshot) {
|
|||||||
"",
|
"",
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
for (const pkg of ["opencode", "opencode-bin"]) {
|
for (const pkg of ["opencode-bin"]) {
|
||||||
await $`rm -rf ./dist/aur-${pkg}`
|
await $`rm -rf ./dist/aur-${pkg}`
|
||||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||||
await $`cd ./dist/aur-${pkg} && git checkout master`
|
await $`cd ./dist/aur-${pkg} && git checkout master`
|
||||||
@@ -136,6 +136,7 @@ if (!snapshot) {
|
|||||||
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
|
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
|
||||||
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
|
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Homebrew formula
|
// Homebrew formula
|
||||||
const homebrewFormula = [
|
const homebrewFormula = [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Provider } from "../provider/provider"
|
|||||||
import { generateObject, type ModelMessage } from "ai"
|
import { generateObject, type ModelMessage } from "ai"
|
||||||
import PROMPT_GENERATE from "./generate.txt"
|
import PROMPT_GENERATE from "./generate.txt"
|
||||||
import { SystemPrompt } from "../session/system"
|
import { SystemPrompt } from "../session/system"
|
||||||
|
import { mergeDeep } from "remeda"
|
||||||
|
|
||||||
export namespace Agent {
|
export namespace Agent {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
@@ -12,8 +13,14 @@ export namespace Agent {
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
|
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
|
||||||
|
builtIn: z.boolean(),
|
||||||
topP: z.number().optional(),
|
topP: z.number().optional(),
|
||||||
temperature: z.number().optional(),
|
temperature: z.number().optional(),
|
||||||
|
permission: z.object({
|
||||||
|
edit: Config.Permission,
|
||||||
|
bash: z.record(z.string(), Config.Permission),
|
||||||
|
webfetch: Config.Permission.optional(),
|
||||||
|
}),
|
||||||
model: z
|
model: z
|
||||||
.object({
|
.object({
|
||||||
modelID: z.string(),
|
modelID: z.string(),
|
||||||
@@ -31,6 +38,15 @@ export namespace Agent {
|
|||||||
|
|
||||||
const state = App.state("agent", async () => {
|
const state = App.state("agent", async () => {
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
|
const defaultPermission: Info["permission"] = {
|
||||||
|
edit: "allow",
|
||||||
|
bash: {
|
||||||
|
"*": "allow",
|
||||||
|
},
|
||||||
|
webfetch: "allow",
|
||||||
|
}
|
||||||
|
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})
|
||||||
|
|
||||||
const result: Record<string, Info> = {
|
const result: Record<string, Info> = {
|
||||||
general: {
|
general: {
|
||||||
name: "general",
|
name: "general",
|
||||||
@@ -41,23 +57,29 @@ export namespace Agent {
|
|||||||
todowrite: false,
|
todowrite: false,
|
||||||
},
|
},
|
||||||
options: {},
|
options: {},
|
||||||
|
permission: agentPermission,
|
||||||
mode: "subagent",
|
mode: "subagent",
|
||||||
|
builtIn: true,
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
name: "build",
|
name: "build",
|
||||||
tools: {},
|
tools: {},
|
||||||
options: {},
|
options: {},
|
||||||
|
permission: agentPermission,
|
||||||
mode: "primary",
|
mode: "primary",
|
||||||
|
builtIn: true,
|
||||||
},
|
},
|
||||||
plan: {
|
plan: {
|
||||||
name: "plan",
|
name: "plan",
|
||||||
options: {},
|
options: {},
|
||||||
|
permission: agentPermission,
|
||||||
tools: {
|
tools: {
|
||||||
write: false,
|
write: false,
|
||||||
edit: false,
|
edit: false,
|
||||||
patch: false,
|
patch: false,
|
||||||
},
|
},
|
||||||
mode: "primary",
|
mode: "primary",
|
||||||
|
builtIn: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
||||||
@@ -70,25 +92,31 @@ export namespace Agent {
|
|||||||
item = result[key] = {
|
item = result[key] = {
|
||||||
name: key,
|
name: key,
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
permission: agentPermission,
|
||||||
options: {},
|
options: {},
|
||||||
tools: {},
|
tools: {},
|
||||||
|
builtIn: false,
|
||||||
}
|
}
|
||||||
const { model, prompt, tools, description, temperature, top_p, mode, ...extra } = value
|
const { model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
|
||||||
item.options = {
|
item.options = {
|
||||||
...item.options,
|
...item.options,
|
||||||
...extra,
|
...extra,
|
||||||
}
|
}
|
||||||
if (value.model) item.model = Provider.parseModel(value.model)
|
if (model) item.model = Provider.parseModel(model)
|
||||||
if (value.prompt) item.prompt = value.prompt
|
if (prompt) item.prompt = prompt
|
||||||
if (value.tools)
|
if (tools)
|
||||||
item.tools = {
|
item.tools = {
|
||||||
...item.tools,
|
...item.tools,
|
||||||
...value.tools,
|
...tools,
|
||||||
}
|
}
|
||||||
if (value.description) item.description = value.description
|
if (description) item.description = description
|
||||||
if (value.temperature != undefined) item.temperature = value.temperature
|
if (temperature != undefined) item.temperature = temperature
|
||||||
if (value.top_p != undefined) item.topP = value.top_p
|
if (top_p != undefined) item.topP = top_p
|
||||||
if (value.mode) item.mode = value.mode
|
if (mode) item.mode = mode
|
||||||
|
|
||||||
|
if (permission ?? cfg.permission) {
|
||||||
|
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
@@ -131,3 +159,32 @@ export namespace Agent {
|
|||||||
return result.object
|
return result.object
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeAgentPermissions(basePermission: any, overridePermission: any): Agent.Info["permission"] {
|
||||||
|
const merged = mergeDeep(basePermission ?? {}, overridePermission ?? {}) as any
|
||||||
|
let mergedBash
|
||||||
|
if (merged.bash) {
|
||||||
|
if (typeof merged.bash === "string") {
|
||||||
|
mergedBash = {
|
||||||
|
"*": merged.bash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if granular permissions are provided, default to "ask"
|
||||||
|
if (typeof merged.bash === "object") {
|
||||||
|
mergedBash = mergeDeep(
|
||||||
|
{
|
||||||
|
"*": "ask",
|
||||||
|
},
|
||||||
|
merged.bash,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Agent.Info["permission"] = {
|
||||||
|
edit: merged.edit ?? "allow",
|
||||||
|
webfetch: merged.webfetch ?? "allow",
|
||||||
|
bash: mergedBash ?? { "*": "allow" },
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import { generatePKCE } from "@openauthjs/openauth/pkce"
|
|
||||||
import { Auth } from "./index"
|
|
||||||
|
|
||||||
export namespace AuthAnthropic {
|
|
||||||
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
|
||||||
|
|
||||||
export async function authorize(mode: "max" | "console") {
|
|
||||||
const pkce = await generatePKCE()
|
|
||||||
|
|
||||||
const url = new URL(
|
|
||||||
`https://${mode === "console" ? "console.anthropic.com" : "claude.ai"}/oauth/authorize`,
|
|
||||||
import.meta.url,
|
|
||||||
)
|
|
||||||
url.searchParams.set("code", "true")
|
|
||||||
url.searchParams.set("client_id", CLIENT_ID)
|
|
||||||
url.searchParams.set("response_type", "code")
|
|
||||||
url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback")
|
|
||||||
url.searchParams.set("scope", "org:create_api_key user:profile user:inference")
|
|
||||||
url.searchParams.set("code_challenge", pkce.challenge)
|
|
||||||
url.searchParams.set("code_challenge_method", "S256")
|
|
||||||
url.searchParams.set("state", pkce.verifier)
|
|
||||||
return {
|
|
||||||
url: url.toString(),
|
|
||||||
verifier: pkce.verifier,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exchange(code: string, verifier: string) {
|
|
||||||
const splits = code.split("#")
|
|
||||||
const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
code: splits[0],
|
|
||||||
state: splits[1],
|
|
||||||
grant_type: "authorization_code",
|
|
||||||
client_id: CLIENT_ID,
|
|
||||||
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
|
|
||||||
code_verifier: verifier,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
if (!result.ok) throw new ExchangeFailed()
|
|
||||||
const json = await result.json()
|
|
||||||
return {
|
|
||||||
refresh: json.refresh_token as string,
|
|
||||||
access: json.access_token as string,
|
|
||||||
expires: Date.now() + json.expires_in * 1000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function access() {
|
|
||||||
const info = await Auth.get("anthropic")
|
|
||||||
if (!info || info.type !== "oauth") return
|
|
||||||
if (info.access && info.expires > Date.now()) return info.access
|
|
||||||
const response = await fetch("https://console.anthropic.com/v1/oauth/token", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
grant_type: "refresh_token",
|
|
||||||
refresh_token: info.refresh,
|
|
||||||
client_id: CLIENT_ID,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
if (!response.ok) return
|
|
||||||
const json = await response.json()
|
|
||||||
await Auth.set("anthropic", {
|
|
||||||
type: "oauth",
|
|
||||||
refresh: json.refresh_token as string,
|
|
||||||
access: json.access_token as string,
|
|
||||||
expires: Date.now() + json.expires_in * 1000,
|
|
||||||
})
|
|
||||||
return json.access_token as string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExchangeFailed extends Error {
|
|
||||||
constructor() {
|
|
||||||
super("Exchange failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Global } from "../global"
|
|
||||||
import { lazy } from "../util/lazy"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
export const AuthCopilot = lazy(async () => {
|
|
||||||
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
|
|
||||||
const exists = await file.exists()
|
|
||||||
const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
|
|
||||||
.then((x) => Bun.write(file, x))
|
|
||||||
.catch(() => {})
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
const worked = await response
|
|
||||||
if (!worked) return
|
|
||||||
}
|
|
||||||
const result = await import(file.name!).catch(() => {})
|
|
||||||
if (!result) return
|
|
||||||
return result.AuthCopilot
|
|
||||||
})
|
|
||||||
@@ -4,25 +4,31 @@ import fs from "fs/promises"
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export namespace Auth {
|
export namespace Auth {
|
||||||
export const Oauth = z.object({
|
export const Oauth = z
|
||||||
type: z.literal("oauth"),
|
.object({
|
||||||
refresh: z.string(),
|
type: z.literal("oauth"),
|
||||||
access: z.string(),
|
refresh: z.string(),
|
||||||
expires: z.number(),
|
access: z.string(),
|
||||||
})
|
expires: z.number(),
|
||||||
|
})
|
||||||
|
.openapi({ ref: "OAuth" })
|
||||||
|
|
||||||
export const Api = z.object({
|
export const Api = z
|
||||||
type: z.literal("api"),
|
.object({
|
||||||
key: z.string(),
|
type: z.literal("api"),
|
||||||
})
|
key: z.string(),
|
||||||
|
})
|
||||||
|
.openapi({ ref: "ApiAuth" })
|
||||||
|
|
||||||
export const WellKnown = z.object({
|
export const WellKnown = z
|
||||||
type: z.literal("wellknown"),
|
.object({
|
||||||
key: z.string(),
|
type: z.literal("wellknown"),
|
||||||
token: z.string(),
|
key: z.string(),
|
||||||
})
|
token: z.string(),
|
||||||
|
})
|
||||||
|
.openapi({ ref: "WellKnownAuth" })
|
||||||
|
|
||||||
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown])
|
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).openapi({ ref: "Auth" })
|
||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
const filepath = path.join(Global.Path.data, "auth.json")
|
const filepath = path.join(Global.Path.data, "auth.json")
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { AuthAnthropic } from "../../auth/anthropic"
|
|
||||||
import { AuthCopilot } from "../../auth/copilot"
|
|
||||||
import { Auth } from "../../auth"
|
import { Auth } from "../../auth"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
@@ -10,6 +8,8 @@ import { map, pipe, sortBy, values } from "remeda"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
|
import { Plugin } from "../../plugin"
|
||||||
|
import { App } from "../../app/app"
|
||||||
|
|
||||||
export const AuthCommand = cmd({
|
export const AuthCommand = cmd({
|
||||||
command: "auth",
|
command: "auth",
|
||||||
@@ -75,242 +75,179 @@ export const AuthLoginCommand = cmd({
|
|||||||
type: "string",
|
type: "string",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
UI.empty()
|
await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
prompts.intro("Add credential")
|
UI.empty()
|
||||||
if (args.url) {
|
prompts.intro("Add credential")
|
||||||
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
|
if (args.url) {
|
||||||
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
|
||||||
const proc = Bun.spawn({
|
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||||
cmd: wellknown.auth.command,
|
const proc = Bun.spawn({
|
||||||
stdout: "pipe",
|
cmd: wellknown.auth.command,
|
||||||
})
|
stdout: "pipe",
|
||||||
const exit = await proc.exited
|
})
|
||||||
if (exit !== 0) {
|
const exit = await proc.exited
|
||||||
prompts.log.error("Failed")
|
if (exit !== 0) {
|
||||||
|
prompts.log.error("Failed")
|
||||||
|
prompts.outro("Done")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const token = await new Response(proc.stdout).text()
|
||||||
|
await Auth.set(args.url, {
|
||||||
|
type: "wellknown",
|
||||||
|
key: wellknown.auth.env,
|
||||||
|
token: token.trim(),
|
||||||
|
})
|
||||||
|
prompts.log.success("Logged into " + args.url)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const token = await new Response(proc.stdout).text()
|
await ModelsDev.refresh().catch(() => {})
|
||||||
await Auth.set(args.url, {
|
const providers = await ModelsDev.get()
|
||||||
type: "wellknown",
|
const priority: Record<string, number> = {
|
||||||
key: wellknown.auth.env,
|
anthropic: 0,
|
||||||
token: token.trim(),
|
"github-copilot": 1,
|
||||||
})
|
openai: 2,
|
||||||
prompts.log.success("Logged into " + args.url)
|
google: 3,
|
||||||
prompts.outro("Done")
|
openrouter: 4,
|
||||||
return
|
vercel: 5,
|
||||||
}
|
}
|
||||||
await ModelsDev.refresh().catch(() => {})
|
let provider = await prompts.autocomplete({
|
||||||
const providers = await ModelsDev.get()
|
message: "Select provider",
|
||||||
const priority: Record<string, number> = {
|
maxItems: 8,
|
||||||
anthropic: 0,
|
|
||||||
"github-copilot": 1,
|
|
||||||
openai: 2,
|
|
||||||
google: 3,
|
|
||||||
openrouter: 4,
|
|
||||||
vercel: 5,
|
|
||||||
}
|
|
||||||
let provider = await prompts.autocomplete({
|
|
||||||
message: "Select provider",
|
|
||||||
maxItems: 8,
|
|
||||||
options: [
|
|
||||||
...pipe(
|
|
||||||
providers,
|
|
||||||
values(),
|
|
||||||
sortBy(
|
|
||||||
(x) => priority[x.id] ?? 99,
|
|
||||||
(x) => x.name ?? x.id,
|
|
||||||
),
|
|
||||||
map((x) => ({
|
|
||||||
label: x.name,
|
|
||||||
value: x.id,
|
|
||||||
hint: priority[x.id] === 0 ? "recommended" : undefined,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
{
|
|
||||||
value: "other",
|
|
||||||
label: "Other",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
if (provider === "other") {
|
|
||||||
provider = await prompts.text({
|
|
||||||
message: "Enter provider id",
|
|
||||||
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
|
||||||
provider = provider.replace(/^@ai-sdk\//, "")
|
|
||||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
|
||||||
prompts.log.warn(
|
|
||||||
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === "amazon-bedrock") {
|
|
||||||
prompts.log.info(
|
|
||||||
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
|
|
||||||
)
|
|
||||||
prompts.outro("Done")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === "anthropic") {
|
|
||||||
const method = await prompts.select({
|
|
||||||
message: "Login method",
|
|
||||||
options: [
|
options: [
|
||||||
|
...pipe(
|
||||||
|
providers,
|
||||||
|
values(),
|
||||||
|
sortBy(
|
||||||
|
(x) => priority[x.id] ?? 99,
|
||||||
|
(x) => x.name ?? x.id,
|
||||||
|
),
|
||||||
|
map((x) => ({
|
||||||
|
label: x.name,
|
||||||
|
value: x.id,
|
||||||
|
hint: priority[x.id] === 0 ? "recommended" : undefined,
|
||||||
|
})),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
label: "Claude Pro/Max",
|
value: "other",
|
||||||
value: "max",
|
label: "Other",
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Create API Key",
|
|
||||||
value: "console",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Manually enter API Key",
|
|
||||||
value: "api",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(method)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
if (method === "max") {
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||||
// some weird bug where program exits without this
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
||||||
const { url, verifier } = await AuthAnthropic.authorize("max")
|
|
||||||
prompts.note("Trying to open browser...")
|
|
||||||
try {
|
|
||||||
await open(url)
|
|
||||||
} catch (e) {
|
|
||||||
prompts.log.error(
|
|
||||||
"Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
prompts.log.info(url)
|
|
||||||
|
|
||||||
const code = await prompts.text({
|
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
|
||||||
message: "Paste the authorization code here: ",
|
if (plugin && plugin.auth) {
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
let index = 0
|
||||||
})
|
if (plugin.auth.methods.length > 1) {
|
||||||
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
const method = await prompts.select({
|
||||||
|
message: "Login method",
|
||||||
try {
|
options: [
|
||||||
const credentials = await AuthAnthropic.exchange(code, verifier)
|
...plugin.auth.methods.map((x, index) => ({
|
||||||
await Auth.set("anthropic", {
|
label: x.label,
|
||||||
type: "oauth",
|
value: index.toString(),
|
||||||
refresh: credentials.refresh,
|
})),
|
||||||
access: credentials.access,
|
],
|
||||||
expires: credentials.expires,
|
|
||||||
})
|
})
|
||||||
prompts.log.success("Login successful")
|
if (prompts.isCancel(method)) throw new UI.CancelledError()
|
||||||
} catch {
|
index = parseInt(method)
|
||||||
prompts.log.error("Invalid code")
|
|
||||||
}
|
}
|
||||||
prompts.outro("Done")
|
const method = plugin.auth.methods[index]
|
||||||
return
|
if (method.type === "oauth") {
|
||||||
}
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
|
const authorize = await method.authorize()
|
||||||
|
|
||||||
if (method === "console") {
|
if (authorize.url) {
|
||||||
// some weird bug where program exits without this
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
await open(authorize.url)
|
||||||
const { url, verifier } = await AuthAnthropic.authorize("console")
|
} catch (e) {}
|
||||||
prompts.note("Trying to open browser...")
|
prompts.log.info("Go to: " + authorize.url)
|
||||||
try {
|
|
||||||
await open(url)
|
|
||||||
} catch (e) {
|
|
||||||
prompts.log.error(
|
|
||||||
"Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
prompts.log.info(url)
|
|
||||||
|
|
||||||
const code = await prompts.text({
|
|
||||||
message: "Paste the authorization code here: ",
|
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const credentials = await AuthAnthropic.exchange(code, verifier)
|
|
||||||
const accessToken = credentials.access
|
|
||||||
const response = await fetch("https://api.anthropic.com/api/oauth/claude_cli/create_api_key", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
Accept: "application/json, text/plain, */*",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to create API key")
|
|
||||||
}
|
}
|
||||||
const json = await response.json()
|
|
||||||
await Auth.set("anthropic", {
|
|
||||||
type: "api",
|
|
||||||
key: json.raw_key,
|
|
||||||
})
|
|
||||||
|
|
||||||
prompts.log.success("Login successful - API key created and saved")
|
if (authorize.method === "auto") {
|
||||||
} catch (error) {
|
if (authorize.instructions) {
|
||||||
prompts.log.error("Invalid code or failed to create API key")
|
prompts.log.info(authorize.instructions)
|
||||||
|
}
|
||||||
|
const spinner = prompts.spinner()
|
||||||
|
spinner.start("Waiting for authorization...")
|
||||||
|
const result = await authorize.callback()
|
||||||
|
if (result.type === "failed") {
|
||||||
|
spinner.stop("Failed to authorize", 1)
|
||||||
|
}
|
||||||
|
if (result.type === "success") {
|
||||||
|
await Auth.set(provider, {
|
||||||
|
type: "oauth",
|
||||||
|
refresh: result.refresh,
|
||||||
|
access: result.access,
|
||||||
|
expires: result.expires,
|
||||||
|
})
|
||||||
|
spinner.stop("Login successful")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorize.method === "code") {
|
||||||
|
const code = await prompts.text({
|
||||||
|
message: "Paste the authorization code here: ",
|
||||||
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
||||||
|
const result = await authorize.callback(code)
|
||||||
|
if (result.type === "failed") {
|
||||||
|
prompts.log.error("Failed to authorize")
|
||||||
|
}
|
||||||
|
if (result.type === "success") {
|
||||||
|
await Auth.set(provider, {
|
||||||
|
type: "oauth",
|
||||||
|
refresh: result.refresh,
|
||||||
|
access: result.access,
|
||||||
|
expires: result.expires,
|
||||||
|
})
|
||||||
|
prompts.log.success("Login successful")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prompts.outro("Done")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === "other") {
|
||||||
|
provider = await prompts.text({
|
||||||
|
message: "Enter provider id",
|
||||||
|
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||||
|
provider = provider.replace(/^@ai-sdk\//, "")
|
||||||
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||||
|
prompts.log.warn(
|
||||||
|
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === "amazon-bedrock") {
|
||||||
|
prompts.log.info(
|
||||||
|
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
|
||||||
|
)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const copilot = await AuthCopilot()
|
if (provider === "vercel") {
|
||||||
if (provider === "github-copilot" && copilot) {
|
prompts.log.info("You can create an api key in the dashboard")
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
||||||
const deviceInfo = await copilot.authorize()
|
|
||||||
|
|
||||||
prompts.note(`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`)
|
|
||||||
|
|
||||||
const spinner = prompts.spinner()
|
|
||||||
spinner.start("Waiting for authorization...")
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, deviceInfo.interval * 1000))
|
|
||||||
const response = await copilot.poll(deviceInfo.device)
|
|
||||||
if (response.status === "pending") continue
|
|
||||||
if (response.status === "success") {
|
|
||||||
await Auth.set("github-copilot", {
|
|
||||||
type: "oauth",
|
|
||||||
refresh: response.refresh,
|
|
||||||
access: response.access,
|
|
||||||
expires: response.expires,
|
|
||||||
})
|
|
||||||
spinner.stop("Login successful")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (response.status === "failed") {
|
|
||||||
spinner.stop("Failed to authorize", 1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const key = await prompts.password({
|
||||||
|
message: "Enter your API key",
|
||||||
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
||||||
|
await Auth.set(provider, {
|
||||||
|
type: "api",
|
||||||
|
key,
|
||||||
|
})
|
||||||
|
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === "vercel") {
|
|
||||||
prompts.log.info("You can create an api key in the dashboard")
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = await prompts.password({
|
|
||||||
message: "Enter your API key",
|
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
||||||
await Auth.set(provider, {
|
|
||||||
type: "api",
|
|
||||||
key,
|
|
||||||
})
|
|
||||||
|
|
||||||
prompts.outro("Done")
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -188,13 +188,17 @@ export const GithubInstallCommand = cmd({
|
|||||||
const info = await $`git remote get-url origin`.quiet().nothrow().text()
|
const info = await $`git remote get-url origin`.quiet().nothrow().text()
|
||||||
// match https or git pattern
|
// match https or git pattern
|
||||||
// ie. https://github.com/sst/opencode.git
|
// ie. https://github.com/sst/opencode.git
|
||||||
|
// ie. https://github.com/sst/opencode
|
||||||
// ie. git@github.com:sst/opencode.git
|
// ie. git@github.com:sst/opencode.git
|
||||||
const parsed = info.match(/git@github\.com:(.*)\.git/) ?? info.match(/github\.com\/(.*)\.git/)
|
// ie. git@github.com:sst/opencode
|
||||||
|
// ie. ssh://git@github.com/sst/opencode.git
|
||||||
|
// ie. ssh://git@github.com/sst/opencode
|
||||||
|
const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||||
throw new UI.CancelledError()
|
throw new UI.CancelledError()
|
||||||
}
|
}
|
||||||
const [owner, repo] = parsed[1].split("/")
|
const [, owner, repo] = parsed
|
||||||
return { owner, repo, root: app.path.root }
|
return { owner, repo, root: app.path.root }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function FormatError(input: unknown) {
|
|||||||
}
|
}
|
||||||
if (Config.InvalidError.isInstance(input))
|
if (Config.InvalidError.isInstance(input))
|
||||||
return [
|
return [
|
||||||
`Config file at ${input.data.path} is invalid`,
|
`Config file at ${input.data.path} is invalid` + (input.data.message ? `: ${input.data.message}` : ""),
|
||||||
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
|
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
|
|||||||
@@ -105,11 +105,15 @@ export namespace Config {
|
|||||||
result.plugin = result.plugin || []
|
result.plugin = result.plugin || []
|
||||||
result.plugin.push(
|
result.plugin.push(
|
||||||
...[
|
...[
|
||||||
...(await Filesystem.globUp("plugin/*.ts", Global.Path.config, Global.Path.config)),
|
...(await Filesystem.globUp("plugin/*.{ts,js}", Global.Path.config, Global.Path.config)),
|
||||||
...(await Filesystem.globUp(".opencode/plugin/*.ts", app.path.cwd, app.path.root)),
|
...(await Filesystem.globUp(".opencode/plugin/*.{ts,js}", app.path.cwd, app.path.root)),
|
||||||
].map((x) => "file://" + x),
|
].map((x) => "file://" + x),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (Flag.OPENCODE_PERMISSION) {
|
||||||
|
result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
|
||||||
|
}
|
||||||
|
|
||||||
// Handle migration from autoshare to share field
|
// Handle migration from autoshare to share field
|
||||||
if (result.autoshare === true && !result.share) {
|
if (result.autoshare === true && !result.share) {
|
||||||
result.share = "auto"
|
result.share = "auto"
|
||||||
@@ -123,6 +127,12 @@ export namespace Config {
|
|||||||
if (result.keybinds?.switch_mode_reverse && !result.keybinds.switch_agent_reverse) {
|
if (result.keybinds?.switch_mode_reverse && !result.keybinds.switch_agent_reverse) {
|
||||||
result.keybinds.switch_agent_reverse = result.keybinds.switch_mode_reverse
|
result.keybinds.switch_agent_reverse = result.keybinds.switch_mode_reverse
|
||||||
}
|
}
|
||||||
|
if (result.keybinds?.switch_agent && !result.keybinds.agent_cycle) {
|
||||||
|
result.keybinds.agent_cycle = result.keybinds.switch_agent
|
||||||
|
}
|
||||||
|
if (result.keybinds?.switch_agent_reverse && !result.keybinds.agent_cycle_reverse) {
|
||||||
|
result.keybinds.agent_cycle_reverse = result.keybinds.switch_agent_reverse
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.username) {
|
if (!result.username) {
|
||||||
const os = await import("os")
|
const os = await import("os")
|
||||||
@@ -164,6 +174,9 @@ export namespace Config {
|
|||||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||||
export type Mcp = z.infer<typeof Mcp>
|
export type Mcp = z.infer<typeof Mcp>
|
||||||
|
|
||||||
|
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
|
||||||
|
export type Permission = z.infer<typeof Permission>
|
||||||
|
|
||||||
export const Agent = z
|
export const Agent = z
|
||||||
.object({
|
.object({
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
@@ -174,6 +187,13 @@ export namespace Config {
|
|||||||
disable: z.boolean().optional(),
|
disable: z.boolean().optional(),
|
||||||
description: z.string().optional().describe("Description of when to use the agent"),
|
description: z.string().optional().describe("Description of when to use the agent"),
|
||||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
|
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
|
||||||
|
permission: z
|
||||||
|
.object({
|
||||||
|
edit: Permission.optional(),
|
||||||
|
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
|
||||||
|
webfetch: Permission.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.catchall(z.any())
|
.catchall(z.any())
|
||||||
.openapi({
|
.openapi({
|
||||||
@@ -185,15 +205,12 @@ export namespace Config {
|
|||||||
.object({
|
.object({
|
||||||
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
||||||
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
|
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
|
||||||
switch_mode: z.string().optional().default("none").describe("@deprecated use switch_agent. Next mode"),
|
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
|
||||||
switch_mode_reverse: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("none")
|
|
||||||
.describe("@deprecated use switch_agent_reverse. Previous mode"),
|
|
||||||
switch_agent: z.string().optional().default("tab").describe("Next agent"),
|
|
||||||
switch_agent_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
|
||||||
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||||
|
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
||||||
|
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),
|
||||||
|
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
|
||||||
|
thinking_blocks: z.string().optional().default("<leader>b").describe("Toggle thinking blocks"),
|
||||||
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
|
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
|
||||||
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
||||||
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
|
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
|
||||||
@@ -201,19 +218,12 @@ export namespace Config {
|
|||||||
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
|
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
|
||||||
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
|
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
|
||||||
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
|
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
|
||||||
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
|
session_child_cycle: z.string().optional().default("ctrl+right").describe("Cycle to next child session"),
|
||||||
thinking_blocks: z.string().optional().default("<leader>b").describe("Toggle thinking blocks"),
|
session_child_cycle_reverse: z
|
||||||
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
.string()
|
||||||
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
.optional()
|
||||||
file_list: z.string().optional().default("<leader>f").describe("List files"),
|
.default("ctrl+left")
|
||||||
file_close: z.string().optional().default("esc").describe("Close file"),
|
.describe("Cycle to previous child session"),
|
||||||
file_search: z.string().optional().default("<leader>/").describe("Search file"),
|
|
||||||
file_diff_toggle: z.string().optional().default("<leader>v").describe("Split/unified diff"),
|
|
||||||
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),
|
|
||||||
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
|
||||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
|
||||||
input_submit: z.string().optional().default("enter").describe("Submit input"),
|
|
||||||
input_newline: z.string().optional().default("shift+enter,ctrl+j").describe("Insert newline in input"),
|
|
||||||
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
|
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
|
||||||
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
|
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
|
||||||
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
|
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
|
||||||
@@ -222,35 +232,63 @@ export namespace Config {
|
|||||||
.optional()
|
.optional()
|
||||||
.default("ctrl+alt+d")
|
.default("ctrl+alt+d")
|
||||||
.describe("Scroll messages down by half page"),
|
.describe("Scroll messages down by half page"),
|
||||||
messages_previous: z.string().optional().default("ctrl+up").describe("Navigate to previous message"),
|
|
||||||
messages_next: z.string().optional().default("ctrl+down").describe("Navigate to next message"),
|
|
||||||
messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"),
|
messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"),
|
||||||
messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"),
|
messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"),
|
||||||
messages_layout_toggle: z.string().optional().default("<leader>p").describe("Toggle layout"),
|
|
||||||
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
|
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
|
||||||
messages_revert: z.string().optional().default("none").describe("@deprecated use messages_undo. Revert message"),
|
|
||||||
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
|
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
|
||||||
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
|
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
|
||||||
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
|
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
||||||
|
model_cycle_recent: z.string().optional().default("f2").describe("Next recent model"),
|
||||||
|
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recent model"),
|
||||||
|
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
|
||||||
|
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
||||||
|
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
||||||
|
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||||
|
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||||
|
input_submit: z.string().optional().default("enter").describe("Submit input"),
|
||||||
|
input_newline: z.string().optional().default("shift+enter,ctrl+j").describe("Insert newline in input"),
|
||||||
|
// Deprecated commands
|
||||||
|
switch_mode: z.string().optional().default("none").describe("@deprecated use agent_cycle. Next mode"),
|
||||||
|
switch_mode_reverse: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("none")
|
||||||
|
.describe("@deprecated use agent_cycle_reverse. Previous mode"),
|
||||||
|
switch_agent: z.string().optional().default("tab").describe("@deprecated use agent_cycle. Next agent"),
|
||||||
|
switch_agent_reverse: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("shift+tab")
|
||||||
|
.describe("@deprecated use agent_cycle_reverse. Previous agent"),
|
||||||
|
file_list: z.string().optional().default("none").describe("@deprecated Currently not available. List files"),
|
||||||
|
file_close: z.string().optional().default("none").describe("@deprecated Close file"),
|
||||||
|
file_search: z.string().optional().default("none").describe("@deprecated Search file"),
|
||||||
|
file_diff_toggle: z.string().optional().default("none").describe("@deprecated Split/unified diff"),
|
||||||
|
messages_previous: z.string().optional().default("none").describe("@deprecated Navigate to previous message"),
|
||||||
|
messages_next: z.string().optional().default("none").describe("@deprecated Navigate to next message"),
|
||||||
|
messages_layout_toggle: z.string().optional().default("none").describe("@deprecated Toggle layout"),
|
||||||
|
messages_revert: z.string().optional().default("none").describe("@deprecated use messages_undo. Revert message"),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "KeybindsConfig",
|
ref: "KeybindsConfig",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const TUI = z.object({
|
||||||
|
scroll_speed: z.number().min(1).optional().default(2).describe("TUI scroll speed"),
|
||||||
|
})
|
||||||
|
|
||||||
export const Layout = z.enum(["auto", "stretch"]).openapi({
|
export const Layout = z.enum(["auto", "stretch"]).openapi({
|
||||||
ref: "LayoutConfig",
|
ref: "LayoutConfig",
|
||||||
})
|
})
|
||||||
export type Layout = z.infer<typeof Layout>
|
export type Layout = z.infer<typeof Layout>
|
||||||
|
|
||||||
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
|
|
||||||
export type Permission = z.infer<typeof Permission>
|
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||||
|
tui: TUI.optional().describe("TUI specific settings"),
|
||||||
plugin: z.string().array().optional(),
|
plugin: z.string().array().optional(),
|
||||||
snapshot: z.boolean().optional(),
|
snapshot: z.boolean().optional(),
|
||||||
share: z
|
share: z
|
||||||
@@ -418,14 +456,14 @@ export namespace Config {
|
|||||||
return load(text, filepath)
|
return load(text, filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(text: string, filepath: string) {
|
async function load(text: string, configFilepath: string) {
|
||||||
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||||
return process.env[varName] || ""
|
return process.env[varName] || ""
|
||||||
})
|
})
|
||||||
|
|
||||||
const fileMatches = text.match(/\{file:[^}]+\}/g)
|
const fileMatches = text.match(/\{file:[^}]+\}/g)
|
||||||
if (fileMatches) {
|
if (fileMatches) {
|
||||||
const configDir = path.dirname(filepath)
|
const configDir = path.dirname(configFilepath)
|
||||||
const lines = text.split("\n")
|
const lines = text.split("\n")
|
||||||
|
|
||||||
for (const match of fileMatches) {
|
for (const match of fileMatches) {
|
||||||
@@ -438,7 +476,20 @@ export namespace Config {
|
|||||||
filePath = path.join(os.homedir(), filePath.slice(2))
|
filePath = path.join(os.homedir(), filePath.slice(2))
|
||||||
}
|
}
|
||||||
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
||||||
const fileContent = (await Bun.file(resolvedPath).text()).trim()
|
const fileContent = (
|
||||||
|
await Bun.file(resolvedPath)
|
||||||
|
.text()
|
||||||
|
.catch((error) => {
|
||||||
|
const errMsg = `bad file reference: "${match}"`
|
||||||
|
if (error.code === "ENOENT") {
|
||||||
|
throw new InvalidError(
|
||||||
|
{ path: configFilepath, message: errMsg + ` ${resolvedPath} does not exist` },
|
||||||
|
{ cause: error },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw new InvalidError({ path: configFilepath, message: errMsg }, { cause: error })
|
||||||
|
})
|
||||||
|
).trim()
|
||||||
// escape newlines/quotes, strip outer quotes
|
// escape newlines/quotes, strip outer quotes
|
||||||
text = text.replace(match, JSON.stringify(fileContent).slice(1, -1))
|
text = text.replace(match, JSON.stringify(fileContent).slice(1, -1))
|
||||||
}
|
}
|
||||||
@@ -463,7 +514,7 @@ export namespace Config {
|
|||||||
.join("\n")
|
.join("\n")
|
||||||
|
|
||||||
throw new JsonError({
|
throw new JsonError({
|
||||||
path: filepath,
|
path: configFilepath,
|
||||||
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
|
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -472,21 +523,21 @@ export namespace Config {
|
|||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
if (!parsed.data.$schema) {
|
if (!parsed.data.$schema) {
|
||||||
parsed.data.$schema = "https://opencode.ai/config.json"
|
parsed.data.$schema = "https://opencode.ai/config.json"
|
||||||
await Bun.write(filepath, JSON.stringify(parsed.data, null, 2))
|
await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2))
|
||||||
}
|
}
|
||||||
const data = parsed.data
|
const data = parsed.data
|
||||||
if (data.plugin) {
|
if (data.plugin) {
|
||||||
for (let i = 0; i < data.plugin?.length; i++) {
|
for (let i = 0; i < data.plugin?.length; i++) {
|
||||||
const plugin = data.plugin[i]
|
const plugin = data.plugin[i]
|
||||||
try {
|
try {
|
||||||
data.plugin[i] = import.meta.resolve(plugin, filepath)
|
data.plugin[i] = import.meta.resolve(plugin, configFilepath)
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidError({ path: filepath, issues: parsed.error.issues })
|
throw new InvalidError({ path: configFilepath, issues: parsed.error.issues })
|
||||||
}
|
}
|
||||||
export const JsonError = NamedError.create(
|
export const JsonError = NamedError.create(
|
||||||
"ConfigJsonError",
|
"ConfigJsonError",
|
||||||
@@ -501,6 +552,7 @@ export namespace Config {
|
|||||||
z.object({
|
z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
issues: z.custom<z.ZodIssue[]>().optional(),
|
issues: z.custom<z.ZodIssue[]>().optional(),
|
||||||
|
message: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export namespace Flag {
|
|||||||
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
|
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
|
||||||
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
|
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
|
||||||
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
|
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
|
||||||
|
export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"]
|
||||||
|
export const OPENCODE_DISABLE_DEFAULT_PLUGINS = truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS")
|
||||||
|
|
||||||
function truthy(key: string) {
|
function truthy(key: string) {
|
||||||
const value = process.env[key]?.toLowerCase()
|
const value = process.env[key]?.toLowerCase()
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export namespace Format {
|
|||||||
const proc = Bun.spawn({
|
const proc = Bun.spawn({
|
||||||
cmd: item.command.map((x) => x.replace("$FILE", file)),
|
cmd: item.command.map((x) => x.replace("$FILE", file)),
|
||||||
cwd: App.info().path.cwd,
|
cwd: App.info().path.cwd,
|
||||||
env: item.environment,
|
env: { ...process.env, ...item.environment },
|
||||||
stdout: "ignore",
|
stdout: "ignore",
|
||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ await Promise.all([
|
|||||||
fs.mkdir(Global.Path.config, { recursive: true }),
|
fs.mkdir(Global.Path.config, { recursive: true }),
|
||||||
fs.mkdir(Global.Path.state, { recursive: true }),
|
fs.mkdir(Global.Path.state, { recursive: true }),
|
||||||
fs.mkdir(Global.Path.log, { recursive: true }),
|
fs.mkdir(Global.Path.log, { recursive: true }),
|
||||||
|
fs.mkdir(Global.Path.bin, { recursive: true }),
|
||||||
])
|
])
|
||||||
|
|
||||||
const CACHE_VERSION = "7"
|
const CACHE_VERSION = "8"
|
||||||
|
|
||||||
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
||||||
.text()
|
.text()
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import { GithubCommand } from "./cli/cmd/github"
|
|||||||
|
|
||||||
const cancel = new AbortController()
|
const cancel = new AbortController()
|
||||||
|
|
||||||
|
try {
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
process.on("unhandledRejection", (e) => {
|
process.on("unhandledRejection", (e) => {
|
||||||
Log.Default.error("rejection", {
|
Log.Default.error("rejection", {
|
||||||
e: e instanceof Error ? e.message : e,
|
e: e instanceof Error ? e.message : e,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
|||||||
".yml": "yaml",
|
".yml": "yaml",
|
||||||
".mjs": "javascript",
|
".mjs": "javascript",
|
||||||
".cjs": "javascript",
|
".cjs": "javascript",
|
||||||
|
".vue": "vue",
|
||||||
".zig": "zig",
|
".zig": "zig",
|
||||||
".zon": "zig",
|
".zon": "zig",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -65,6 +65,67 @@ export namespace LSPServer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Vue: Info = {
|
||||||
|
id: "vue",
|
||||||
|
extensions: [".vue"],
|
||||||
|
root: NearestRoot([
|
||||||
|
"tsconfig.json",
|
||||||
|
"jsconfig.json",
|
||||||
|
"package.json",
|
||||||
|
"pnpm-lock.yaml",
|
||||||
|
"yarn.lock",
|
||||||
|
"bun.lockb",
|
||||||
|
"bun.lock",
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.js",
|
||||||
|
"nuxt.config.ts",
|
||||||
|
"nuxt.config.js",
|
||||||
|
"vue.config.js",
|
||||||
|
]),
|
||||||
|
async spawn(_, root) {
|
||||||
|
let binary = Bun.which("vue-language-server")
|
||||||
|
const args: string[] = []
|
||||||
|
if (!binary) {
|
||||||
|
const js = path.join(
|
||||||
|
Global.Path.bin,
|
||||||
|
"node_modules",
|
||||||
|
"@vue",
|
||||||
|
"language-server",
|
||||||
|
"bin",
|
||||||
|
"vue-language-server.js",
|
||||||
|
)
|
||||||
|
if (!(await Bun.file(js).exists())) {
|
||||||
|
await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
|
||||||
|
cwd: Global.Path.bin,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
stdin: "pipe",
|
||||||
|
}).exited
|
||||||
|
}
|
||||||
|
binary = BunProc.which()
|
||||||
|
args.push("run", js)
|
||||||
|
}
|
||||||
|
args.push("--stdio")
|
||||||
|
const proc = spawn(binary, args, {
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
process: proc,
|
||||||
|
initialization: {
|
||||||
|
// Leave empty; the server will auto-detect workspace TypeScript.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const ESLint: Info = {
|
export const ESLint: Info = {
|
||||||
id: "eslint",
|
id: "eslint",
|
||||||
root: NearestRoot([
|
root: NearestRoot([
|
||||||
@@ -81,7 +142,7 @@ export namespace LSPServer {
|
|||||||
".eslintrc.json",
|
".eslintrc.json",
|
||||||
"package.json",
|
"package.json",
|
||||||
]),
|
]),
|
||||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
|
||||||
async spawn(app, root) {
|
async spawn(app, root) {
|
||||||
const eslint = await Bun.resolve("eslint", app.path.cwd).catch(() => {})
|
const eslint = await Bun.resolve("eslint", app.path.cwd).catch(() => {})
|
||||||
if (!eslint) return
|
if (!eslint) return
|
||||||
@@ -94,7 +155,7 @@ export namespace LSPServer {
|
|||||||
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
|
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
|
||||||
await Bun.file(zipPath).write(response)
|
await Bun.file(zipPath).write(response)
|
||||||
|
|
||||||
await $`unzip -o -q ${zipPath}`.cwd(Global.Path.bin).nothrow()
|
await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
|
||||||
await fs.rm(zipPath, { force: true })
|
await fs.rm(zipPath, { force: true })
|
||||||
|
|
||||||
const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
|
const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
|
||||||
@@ -269,7 +330,7 @@ export namespace LSPServer {
|
|||||||
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
|
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
|
||||||
await Bun.file(zipPath).write(response)
|
await Bun.file(zipPath).write(response)
|
||||||
|
|
||||||
await $`unzip -o -q ${zipPath}`.cwd(Global.Path.bin).nothrow()
|
await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
|
||||||
|
|
||||||
await fs.rm(zipPath, {
|
await fs.rm(zipPath, {
|
||||||
force: true,
|
force: true,
|
||||||
@@ -371,7 +432,7 @@ export namespace LSPServer {
|
|||||||
await Bun.file(tempPath).write(downloadResponse)
|
await Bun.file(tempPath).write(downloadResponse)
|
||||||
|
|
||||||
if (ext === "zip") {
|
if (ext === "zip") {
|
||||||
await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
await $`unzip -o -q ${tempPath}`.quiet().cwd(Global.Path.bin).nothrow()
|
||||||
} else {
|
} else {
|
||||||
await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
||||||
}
|
}
|
||||||
@@ -490,7 +551,7 @@ export namespace LSPServer {
|
|||||||
const zipPath = path.join(Global.Path.bin, "clangd.zip")
|
const zipPath = path.join(Global.Path.bin, "clangd.zip")
|
||||||
await Bun.file(zipPath).write(downloadResponse)
|
await Bun.file(zipPath).write(downloadResponse)
|
||||||
|
|
||||||
await $`unzip -o -q ${zipPath}`.cwd(Global.Path.bin).nothrow()
|
await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
|
||||||
await fs.rm(zipPath, { force: true })
|
await fs.rm(zipPath, { force: true })
|
||||||
|
|
||||||
const extractedDir = path.join(Global.Path.bin, assetName.replace(".zip", ""))
|
const extractedDir = path.join(Global.Path.bin, assetName.replace(".zip", ""))
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export namespace Permission {
|
|||||||
async (state) => {
|
async (state) => {
|
||||||
for (const pending of Object.values(state.pending)) {
|
for (const pending of Object.values(state.pending)) {
|
||||||
for (const item of Object.values(pending)) {
|
for (const item of Object.values(pending)) {
|
||||||
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID))
|
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -82,11 +82,13 @@ export namespace Permission {
|
|||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: input.messageID,
|
messageID: input.messageID,
|
||||||
toolCallID: input.callID,
|
toolCallID: input.callID,
|
||||||
|
pattern: input.pattern,
|
||||||
})
|
})
|
||||||
if (approved[input.sessionID]?.[input.pattern ?? input.type]) return
|
if (approved[input.sessionID]?.[input.pattern ?? input.type]) return
|
||||||
const info: Info = {
|
const info: Info = {
|
||||||
id: Identifier.ascending("permission"),
|
id: Identifier.ascending("permission"),
|
||||||
type: input.type,
|
type: input.type,
|
||||||
|
pattern: input.pattern,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: input.messageID,
|
messageID: input.messageID,
|
||||||
callID: input.callID,
|
callID: input.callID,
|
||||||
@@ -103,7 +105,7 @@ export namespace Permission {
|
|||||||
}).then((x) => x.status)
|
}).then((x) => x.status)
|
||||||
) {
|
) {
|
||||||
case "deny":
|
case "deny":
|
||||||
throw new RejectedError(info.sessionID, info.id, info.callID)
|
throw new RejectedError(info.sessionID, info.id, info.callID, info.metadata)
|
||||||
case "allow":
|
case "allow":
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -129,7 +131,7 @@ export namespace Permission {
|
|||||||
if (!match) return
|
if (!match) return
|
||||||
delete pending[input.sessionID][input.permissionID]
|
delete pending[input.sessionID][input.permissionID]
|
||||||
if (input.response === "reject") {
|
if (input.response === "reject") {
|
||||||
match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID))
|
match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
match.resolve()
|
match.resolve()
|
||||||
@@ -154,8 +156,9 @@ export namespace Permission {
|
|||||||
public readonly sessionID: string,
|
public readonly sessionID: string,
|
||||||
public readonly permissionID: string,
|
public readonly permissionID: string,
|
||||||
public readonly toolCallID?: string,
|
public readonly toolCallID?: string,
|
||||||
|
public readonly metadata?: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
super(`The user rejected permission to use this functionality`)
|
super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Log } from "../util/log"
|
|||||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||||
import { Server } from "../server/server"
|
import { Server } from "../server/server"
|
||||||
import { BunProc } from "../bun"
|
import { BunProc } from "../bun"
|
||||||
|
import { Flag } from "../flag/flag"
|
||||||
|
|
||||||
export namespace Plugin {
|
export namespace Plugin {
|
||||||
const log = Log.create({ service: "plugin" })
|
const log = Log.create({ service: "plugin" })
|
||||||
@@ -17,7 +18,17 @@ export namespace Plugin {
|
|||||||
})
|
})
|
||||||
const config = await Config.get()
|
const config = await Config.get()
|
||||||
const hooks = []
|
const hooks = []
|
||||||
for (let plugin of config.plugin ?? []) {
|
const input = {
|
||||||
|
client,
|
||||||
|
app,
|
||||||
|
$: Bun.$,
|
||||||
|
}
|
||||||
|
const plugins = [...(config.plugin ?? [])]
|
||||||
|
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
||||||
|
plugins.push("opencode-copilot-auth")
|
||||||
|
plugins.push("opencode-anthropic-auth")
|
||||||
|
}
|
||||||
|
for (let plugin of plugins) {
|
||||||
log.info("loading plugin", { path: plugin })
|
log.info("loading plugin", { path: plugin })
|
||||||
if (!plugin.startsWith("file://")) {
|
if (!plugin.startsWith("file://")) {
|
||||||
const [pkg, version] = plugin.split("@")
|
const [pkg, version] = plugin.split("@")
|
||||||
@@ -25,22 +36,19 @@ export namespace Plugin {
|
|||||||
}
|
}
|
||||||
const mod = await import(plugin)
|
const mod = await import(plugin)
|
||||||
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
|
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
|
||||||
const init = await fn({
|
const init = await fn(input)
|
||||||
client,
|
|
||||||
app,
|
|
||||||
$: Bun.$,
|
|
||||||
})
|
|
||||||
hooks.push(init)
|
hooks.push(init)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hooks,
|
hooks,
|
||||||
|
input,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function trigger<
|
export async function trigger<
|
||||||
Name extends keyof Required<Hooks>,
|
Name extends Exclude<keyof Required<Hooks>, "auth" | "event">,
|
||||||
Input = Parameters<Required<Hooks>[Name]>[0],
|
Input = Parameters<Required<Hooks>[Name]>[0],
|
||||||
Output = Parameters<Required<Hooks>[Name]>[1],
|
Output = Parameters<Required<Hooks>[Name]>[1],
|
||||||
>(name: Name, input: Input, output: Output): Promise<Output> {
|
>(name: Name, input: Input, output: Output): Promise<Output> {
|
||||||
@@ -56,6 +64,10 @@ export namespace Plugin {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function list() {
|
||||||
|
return state().then((x) => x.hooks)
|
||||||
|
}
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
Bus.subscribeAll(async (input) => {
|
Bus.subscribeAll(async (input) => {
|
||||||
const hooks = await state().then((x) => x.hooks)
|
const hooks = await state().then((x) => x.hooks)
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { mergeDeep, sortBy } from "remeda"
|
|||||||
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { BunProc } from "../bun"
|
import { BunProc } from "../bun"
|
||||||
import { AuthAnthropic } from "../auth/anthropic"
|
import { Plugin } from "../plugin"
|
||||||
import { AuthCopilot } from "../auth/copilot"
|
|
||||||
import { ModelsDev } from "./models"
|
import { ModelsDev } from "./models"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
@@ -26,103 +25,13 @@ export namespace Provider {
|
|||||||
type Source = "env" | "config" | "custom" | "api"
|
type Source = "env" | "config" | "custom" | "api"
|
||||||
|
|
||||||
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
||||||
async anthropic(provider) {
|
async anthropic() {
|
||||||
const access = await AuthAnthropic.access()
|
|
||||||
if (!access)
|
|
||||||
return {
|
|
||||||
autoload: false,
|
|
||||||
options: {
|
|
||||||
headers: {
|
|
||||||
"anthropic-beta":
|
|
||||||
"claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for (const model of Object.values(provider.models)) {
|
|
||||||
model.cost = {
|
|
||||||
input: 0,
|
|
||||||
output: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
autoload: true,
|
autoload: false,
|
||||||
options: {
|
options: {
|
||||||
apiKey: "",
|
headers: {
|
||||||
async fetch(input: any, init: any) {
|
"anthropic-beta":
|
||||||
const access = await AuthAnthropic.access()
|
"claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
||||||
const headers = {
|
|
||||||
...init.headers,
|
|
||||||
authorization: `Bearer ${access}`,
|
|
||||||
"anthropic-beta":
|
|
||||||
"oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
|
||||||
}
|
|
||||||
delete headers["x-api-key"]
|
|
||||||
return fetch(input, {
|
|
||||||
...init,
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"github-copilot": async (provider) => {
|
|
||||||
const copilot = await AuthCopilot()
|
|
||||||
if (!copilot) return { autoload: false }
|
|
||||||
let info = await Auth.get("github-copilot")
|
|
||||||
if (!info || info.type !== "oauth") return { autoload: false }
|
|
||||||
|
|
||||||
if (provider && provider.models) {
|
|
||||||
for (const model of Object.values(provider.models)) {
|
|
||||||
model.cost = {
|
|
||||||
input: 0,
|
|
||||||
output: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
autoload: true,
|
|
||||||
options: {
|
|
||||||
apiKey: "",
|
|
||||||
async fetch(input: any, init: any) {
|
|
||||||
const info = await Auth.get("github-copilot")
|
|
||||||
if (!info || info.type !== "oauth") return
|
|
||||||
if (!info.access || info.expires < Date.now()) {
|
|
||||||
const tokens = await copilot.access(info.refresh)
|
|
||||||
if (!tokens) throw new Error("GitHub Copilot authentication expired")
|
|
||||||
await Auth.set("github-copilot", {
|
|
||||||
type: "oauth",
|
|
||||||
...tokens,
|
|
||||||
})
|
|
||||||
info.access = tokens.access
|
|
||||||
}
|
|
||||||
let isAgentCall = false
|
|
||||||
let isVisionRequest = false
|
|
||||||
try {
|
|
||||||
const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
|
|
||||||
if (body?.messages) {
|
|
||||||
isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role))
|
|
||||||
isVisionRequest = body.messages.some(
|
|
||||||
(msg: any) =>
|
|
||||||
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
...init.headers,
|
|
||||||
...copilot.HEADERS,
|
|
||||||
Authorization: `Bearer ${info.access}`,
|
|
||||||
"Openai-Intent": "conversation-edits",
|
|
||||||
"X-Initiator": isAgentCall ? "agent" : "user",
|
|
||||||
}
|
|
||||||
if (isVisionRequest) {
|
|
||||||
headers["Copilot-Vision-Request"] = "true"
|
|
||||||
}
|
|
||||||
delete headers["x-api-key"]
|
|
||||||
return fetch(input, {
|
|
||||||
...init,
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -350,6 +259,17 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const plugin of await Plugin.list()) {
|
||||||
|
if (!plugin.auth) continue
|
||||||
|
const providerID = plugin.auth.provider
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
|
const auth = await Auth.get(providerID)
|
||||||
|
if (!auth) continue
|
||||||
|
if (!plugin.auth.loader) continue
|
||||||
|
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
|
||||||
|
mergeProvider(plugin.auth.provider, options ?? {}, "custom")
|
||||||
|
}
|
||||||
|
|
||||||
// load config
|
// load config
|
||||||
for (const [providerID, provider] of configProviders) {
|
for (const [providerID, provider] of configProviders) {
|
||||||
mergeProvider(providerID, provider.options ?? {}, "config")
|
mergeProvider(providerID, provider.options ?? {}, "config")
|
||||||
|
|||||||
@@ -83,32 +83,19 @@ export namespace ProviderTransform {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function options(providerID: string, modelID: string): Record<string, any> | undefined {
|
export function options(providerID: string, modelID: string, sessionID: string): Record<string, any> | undefined {
|
||||||
if (modelID.includes("gpt-5")) {
|
const result: Record<string, any> = {}
|
||||||
if (providerID === "azure") {
|
|
||||||
return {
|
if (providerID === "openai") {
|
||||||
reasoningEffort: "minimal",
|
result["promptCacheKey"] = sessionID
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return {
|
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
|
||||||
reasoningEffort: "minimal",
|
result["reasoningEffort"] = "minimal"
|
||||||
textVerbosity: "low",
|
if (providerID !== "azure") {
|
||||||
// reasoningSummary: "auto",
|
result["textVerbosity"] = "low"
|
||||||
// include: ["reasoning.encrypted_content"],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if (modelID.includes("claude")) {
|
return result
|
||||||
// return {
|
|
||||||
// thinking: {
|
|
||||||
// type: "enabled",
|
|
||||||
// budgetTokens: 32000,
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if (_providerID === "bedrock") {
|
|
||||||
// return {
|
|
||||||
// reasoningConfig: { type: "enabled", budgetTokens: 32000 },
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { callTui, TuiRoute } from "./tui"
|
|||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
|
import { Auth } from "../auth"
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
400: {
|
400: {
|
||||||
@@ -88,7 +89,7 @@ export namespace Server {
|
|||||||
version: "0.0.3",
|
version: "0.0.3",
|
||||||
description: "opencode api",
|
description: "opencode api",
|
||||||
},
|
},
|
||||||
openapi: "3.0.0",
|
openapi: "3.1.1",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -247,6 +248,34 @@ export namespace Server {
|
|||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.get(
|
||||||
|
"/session/:id/children",
|
||||||
|
describeRoute({
|
||||||
|
description: "Get a session's children",
|
||||||
|
operationId: "session.children",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "List of children",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Session.Info.array()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const sessionID = c.req.valid("param").id
|
||||||
|
const session = await Session.children(sessionID)
|
||||||
|
return c.json(session)
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/session",
|
"/session",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -264,8 +293,18 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
parentID: z.string().optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const session = await Session.create()
|
const body = c.req.valid("json") ?? {}
|
||||||
|
const session = await Session.create(body.parentID, body.title)
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -296,6 +335,47 @@ export namespace Server {
|
|||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.patch(
|
||||||
|
"/session/:id",
|
||||||
|
describeRoute({
|
||||||
|
description: "Update session properties",
|
||||||
|
operationId: "session.update",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully updated session",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Session.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const sessionID = c.req.valid("param").id
|
||||||
|
const updates = c.req.valid("json")
|
||||||
|
|
||||||
|
const updatedSession = await Session.update(sessionID, (session) => {
|
||||||
|
if (updates.title !== undefined) {
|
||||||
|
session.title = updates.title
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json(updatedSession)
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/session/:id/init",
|
"/session/:id/init",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -551,6 +631,36 @@ export namespace Server {
|
|||||||
return c.json(msg)
|
return c.json(msg)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/session/:id/shell",
|
||||||
|
describeRoute({
|
||||||
|
description: "Run a shell command",
|
||||||
|
operationId: "session.shell",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Created message",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(MessageV2.Assistant),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string().openapi({ description: "Session ID" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
zValidator("json", Session.CommandInput.omit({ sessionID: true })),
|
||||||
|
async (c) => {
|
||||||
|
const sessionID = c.req.valid("param").id
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
const msg = await Session.shell({ ...body, sessionID })
|
||||||
|
return c.json(msg)
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/session/:id/revert",
|
"/session/:id/revert",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -1027,7 +1137,7 @@ export namespace Server {
|
|||||||
.post(
|
.post(
|
||||||
"/tui/execute-command",
|
"/tui/execute-command",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "Execute a TUI command (e.g. switch_agent)",
|
description: "Execute a TUI command (e.g. agent_cycle)",
|
||||||
operationId: "tui.executeCommand",
|
operationId: "tui.executeCommand",
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
@@ -1048,7 +1158,64 @@ export namespace Server {
|
|||||||
),
|
),
|
||||||
async (c) => c.json(await callTui(c)),
|
async (c) => c.json(await callTui(c)),
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/tui/show-toast",
|
||||||
|
describeRoute({
|
||||||
|
description: "Show a toast notification in the TUI",
|
||||||
|
operationId: "tui.showToast",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Toast notification shown successfully",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
message: z.string(),
|
||||||
|
variant: z.enum(["info", "success", "warning", "error"]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => c.json(await callTui(c)),
|
||||||
|
)
|
||||||
.route("/tui/control", TuiRoute)
|
.route("/tui/control", TuiRoute)
|
||||||
|
.put(
|
||||||
|
"/auth/:id",
|
||||||
|
describeRoute({
|
||||||
|
description: "Set authentication credentials",
|
||||||
|
operationId: "auth.set",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully set authentication credentials",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...ERRORS,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
zValidator("json", Auth.Info),
|
||||||
|
async (c) => {
|
||||||
|
const id = c.req.valid("param").id
|
||||||
|
const info = c.req.valid("json")
|
||||||
|
await Auth.set(id, info)
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
@@ -1062,7 +1229,7 @@ export namespace Server {
|
|||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "opencode api",
|
description: "opencode api",
|
||||||
},
|
},
|
||||||
openapi: "3.0.0",
|
openapi: "3.1.1",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { spawn } from "child_process"
|
||||||
import { Decimal } from "decimal.js"
|
import { Decimal } from "decimal.js"
|
||||||
import { z, ZodSchema } from "zod"
|
import { z, ZodSchema } from "zod"
|
||||||
import {
|
import {
|
||||||
@@ -42,6 +43,9 @@ import { ToolRegistry } from "../tool/registry"
|
|||||||
import { Plugin } from "../plugin"
|
import { Plugin } from "../plugin"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
|
import { Wildcard } from "../util/wildcard"
|
||||||
|
import { ulid } from "ulid"
|
||||||
|
import { defer } from "../util/defer"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
const log = Log.create({ service: "session" })
|
const log = Log.create({ service: "session" })
|
||||||
@@ -159,12 +163,12 @@ export namespace Session {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export async function create(parentID?: string) {
|
export async function create(parentID?: string, title?: string) {
|
||||||
const result: Info = {
|
const result: Info = {
|
||||||
id: Identifier.descending("session"),
|
id: Identifier.descending("session"),
|
||||||
version: Installation.VERSION,
|
version: Installation.VERSION,
|
||||||
parentID,
|
parentID,
|
||||||
title: createDefaultTitle(!!parentID),
|
title: title ?? createDefaultTitle(!!parentID),
|
||||||
time: {
|
time: {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
updated: Date.now(),
|
updated: Date.now(),
|
||||||
@@ -522,7 +526,9 @@ export namespace Session {
|
|||||||
t.execute(args, {
|
t.execute(args, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
|
agent: input.agent!,
|
||||||
messageID: userMsg.id,
|
messageID: userMsg.id,
|
||||||
|
extra: { bypassCwdCheck: true },
|
||||||
metadata: async () => {},
|
metadata: async () => {},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -665,14 +671,14 @@ export namespace Session {
|
|||||||
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
|
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
|
||||||
if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
|
if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
|
||||||
|
|
||||||
if (msgs.length === 1 && !session.parentID && isDefaultTitle(session.title)) {
|
if (msgs.filter((m) => m.info.role === "user").length === 1 && !session.parentID && isDefaultTitle(session.title)) {
|
||||||
const small = (await Provider.getSmallModel(input.providerID)) ?? model
|
const small = (await Provider.getSmallModel(input.providerID)) ?? model
|
||||||
generateText({
|
generateText({
|
||||||
maxOutputTokens: small.info.reasoning ? 1024 : 20,
|
maxOutputTokens: small.info.reasoning ? 1024 : 20,
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
[input.providerID]: {
|
[input.providerID]: {
|
||||||
...small.info.options,
|
...small.info.options,
|
||||||
...ProviderTransform.options(input.providerID, small.info.id),
|
...ProviderTransform.options(input.providerID, small.info.id, input.sessionID),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
messages: [
|
messages: [
|
||||||
@@ -758,17 +764,22 @@ export namespace Session {
|
|||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
}
|
}
|
||||||
await updateMessage(assistantMsg)
|
await updateMessage(assistantMsg)
|
||||||
|
await using _ = defer(async () => {
|
||||||
|
if (assistantMsg.time.completed) return
|
||||||
|
await Storage.remove(`session/message/${input.sessionID}/${assistantMsg.id}`)
|
||||||
|
await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: assistantMsg.id })
|
||||||
|
})
|
||||||
const tools: Record<string, AITool> = {}
|
const tools: Record<string, AITool> = {}
|
||||||
|
|
||||||
const processor = createProcessor(assistantMsg, model.info)
|
const processor = createProcessor(assistantMsg, model.info)
|
||||||
|
|
||||||
const enabledTools = pipe(
|
const enabledTools = pipe(
|
||||||
agent.tools,
|
agent.tools,
|
||||||
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID)),
|
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID, agent)),
|
||||||
mergeDeep(input.tools ?? {}),
|
mergeDeep(input.tools ?? {}),
|
||||||
)
|
)
|
||||||
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
|
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
|
||||||
if (enabledTools[item.id] === false) continue
|
if (Wildcard.all(item.id, enabledTools) === false) continue
|
||||||
tools[item.id] = tool({
|
tools[item.id] = tool({
|
||||||
id: item.id as any,
|
id: item.id as any,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
@@ -790,6 +801,7 @@ export namespace Session {
|
|||||||
abort: options.abortSignal!,
|
abort: options.abortSignal!,
|
||||||
messageID: assistantMsg.id,
|
messageID: assistantMsg.id,
|
||||||
callID: options.toolCallId,
|
callID: options.toolCallId,
|
||||||
|
agent: agent.name,
|
||||||
metadata: async (val) => {
|
metadata: async (val) => {
|
||||||
const match = processor.partFromToolCall(options.toolCallId)
|
const match = processor.partFromToolCall(options.toolCallId)
|
||||||
if (match && match.state.status === "running") {
|
if (match && match.state.status === "running") {
|
||||||
@@ -829,7 +841,7 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, item] of Object.entries(await MCP.tools())) {
|
for (const [key, item] of Object.entries(await MCP.tools())) {
|
||||||
if (enabledTools[key] === false) continue
|
if (Wildcard.all(key, enabledTools) === false) continue
|
||||||
const execute = item.execute
|
const execute = item.execute
|
||||||
if (!execute) continue
|
if (!execute) continue
|
||||||
item.execute = async (args, opts) => {
|
item.execute = async (args, opts) => {
|
||||||
@@ -865,7 +877,7 @@ export namespace Session {
|
|||||||
: undefined,
|
: undefined,
|
||||||
topP: agent.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
|
topP: agent.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
|
||||||
options: {
|
options: {
|
||||||
...ProviderTransform.options(input.providerID, input.modelID),
|
...ProviderTransform.options(input.providerID, input.modelID, input.sessionID),
|
||||||
...model.info.options,
|
...model.info.options,
|
||||||
...agent.options,
|
...agent.options,
|
||||||
},
|
},
|
||||||
@@ -961,7 +973,7 @@ export namespace Session {
|
|||||||
content: x,
|
content: x,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
...MessageV2.toModelMessage(msgs),
|
...MessageV2.toModelMessage(msgs.filter((m) => !(m.info.role === "assistant" && m.info.error))),
|
||||||
],
|
],
|
||||||
tools: model.info.tool_call === false ? undefined : tools,
|
tools: model.info.tool_call === false ? undefined : tools,
|
||||||
model: wrapLanguageModel({
|
model: wrapLanguageModel({
|
||||||
@@ -993,6 +1005,136 @@ export namespace Session {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CommandInput = z.object({
|
||||||
|
sessionID: Identifier.schema("session"),
|
||||||
|
agent: z.string(),
|
||||||
|
command: z.string(),
|
||||||
|
})
|
||||||
|
export type CommandInput = z.infer<typeof CommandInput>
|
||||||
|
export async function shell(input: CommandInput) {
|
||||||
|
using abort = lock(input.sessionID)
|
||||||
|
const msg: MessageV2.Assistant = {
|
||||||
|
id: Identifier.ascending("message"),
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
system: [],
|
||||||
|
mode: input.agent,
|
||||||
|
cost: 0,
|
||||||
|
path: {
|
||||||
|
cwd: App.info().path.cwd,
|
||||||
|
root: App.info().path.root,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
created: Date.now(),
|
||||||
|
},
|
||||||
|
role: "assistant",
|
||||||
|
tokens: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
|
},
|
||||||
|
modelID: "",
|
||||||
|
providerID: "",
|
||||||
|
}
|
||||||
|
await updateMessage(msg)
|
||||||
|
const part: MessageV2.Part = {
|
||||||
|
type: "tool",
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
messageID: msg.id,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
tool: "bash",
|
||||||
|
callID: ulid(),
|
||||||
|
state: {
|
||||||
|
status: "running",
|
||||||
|
time: {
|
||||||
|
start: Date.now(),
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
command: input.command,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await updatePart(part)
|
||||||
|
const app = App.info()
|
||||||
|
const shell = process.env["SHELL"] ?? "bash"
|
||||||
|
const shellName = path.basename(shell)
|
||||||
|
|
||||||
|
const scripts: Record<string, string> = {
|
||||||
|
nu: input.command,
|
||||||
|
fish: `eval "${input.command}"`,
|
||||||
|
}
|
||||||
|
|
||||||
|
const script =
|
||||||
|
scripts[shellName] ??
|
||||||
|
`[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
|
||||||
|
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
|
||||||
|
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
|
||||||
|
eval "${input.command}"`
|
||||||
|
|
||||||
|
const isFishOrNu = shellName === "fish" || shellName === "nu"
|
||||||
|
const args = isFishOrNu ? ["-c", script] : ["-c", "-l", script]
|
||||||
|
|
||||||
|
const proc = spawn(shell, args, {
|
||||||
|
cwd: app.path.cwd,
|
||||||
|
signal: abort.signal,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
TERM: "dumb",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let output = ""
|
||||||
|
|
||||||
|
proc.stdout?.on("data", (chunk) => {
|
||||||
|
output += chunk.toString()
|
||||||
|
if (part.state.status === "running") {
|
||||||
|
part.state.metadata = {
|
||||||
|
output: output,
|
||||||
|
description: "",
|
||||||
|
}
|
||||||
|
updatePart(part)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.stderr?.on("data", (chunk) => {
|
||||||
|
output += chunk.toString()
|
||||||
|
if (part.state.status === "running") {
|
||||||
|
part.state.metadata = {
|
||||||
|
output: output,
|
||||||
|
description: "",
|
||||||
|
}
|
||||||
|
updatePart(part)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
proc.on("close", () => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
msg.time.completed = Date.now()
|
||||||
|
await updateMessage(msg)
|
||||||
|
if (part.state.status === "running") {
|
||||||
|
part.state = {
|
||||||
|
status: "completed",
|
||||||
|
time: {
|
||||||
|
...part.state.time,
|
||||||
|
end: Date.now(),
|
||||||
|
},
|
||||||
|
input: part.state.input,
|
||||||
|
title: "",
|
||||||
|
metadata: {
|
||||||
|
output,
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
output,
|
||||||
|
}
|
||||||
|
await updatePart(part)
|
||||||
|
}
|
||||||
|
return { info: msg, parts: [part] }
|
||||||
|
}
|
||||||
|
|
||||||
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
|
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
|
||||||
const toolcalls: Record<string, MessageV2.ToolPart> = {}
|
const toolcalls: Record<string, MessageV2.ToolPart> = {}
|
||||||
let snapshot: string | undefined
|
let snapshot: string | undefined
|
||||||
@@ -1128,6 +1270,7 @@ export namespace Session {
|
|||||||
status: "error",
|
status: "error",
|
||||||
input: value.input,
|
input: value.input,
|
||||||
error: (value.error as any).toString(),
|
error: (value.error as any).toString(),
|
||||||
|
metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined,
|
||||||
time: {
|
time: {
|
||||||
start: match.state.time.start,
|
start: match.state.time.start,
|
||||||
end: Date.now(),
|
end: Date.now(),
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export namespace MessageV2 {
|
|||||||
status: z.literal("error"),
|
status: z.literal("error"),
|
||||||
input: z.record(z.any()),
|
input: z.record(z.any()),
|
||||||
error: z.string(),
|
error: z.string(),
|
||||||
|
metadata: z.record(z.any()).optional(),
|
||||||
time: z.object({
|
time: z.object({
|
||||||
start: z.number(),
|
start: z.number(),
|
||||||
end: z.number(),
|
end: z.number(),
|
||||||
|
|||||||
@@ -1,76 +1,80 @@
|
|||||||
You are an expert AI programming assistant
|
You are an expert AI programming assistant
|
||||||
When asked for your name, you must respond with "opencode".
|
Your name is opencode
|
||||||
Follow the user's requirements carefully & to the letter.
|
|
||||||
Keep your answers short and impersonal.
|
Keep your answers short and impersonal.
|
||||||
<instructions>
|
<gptAgentInstructions>
|
||||||
You are a highly sophisticated automated coding agent with expert-level knowledge across many different programming languages and frameworks.
|
You are a highly sophisticated coding agent with expert-level knowledge across programming languages and frameworks.
|
||||||
The user will ask a question, or ask you to perform a task, and it may require lots of research to answer correctly. There is a selection of tools that let you perform actions or retrieve helpful context to answer the user's question.
|
You are an agent - you must keep going until the user's query is completely resolved, before ending your turn and yielding back to the user.
|
||||||
You are an agent—keep going until the user's query is completely resolved before ending your turn. ONLY stop if solved or genuinely blocked.
|
Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
|
||||||
Take action when possible; the user expects you to do useful work without unnecessary questions.
|
You MUST iterate and keep going until the problem is solved.
|
||||||
After any parallel, read-only context gathering, give a concise progress update and what's next.
|
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
|
||||||
Avoid repetition across turns: don't restate unchanged plans or sections (like the todo list) verbatim; provide delta updates or only the parts that changed.
|
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
|
||||||
Tool batches: You MUST preface each batch with a one-sentence why/what/outcome preamble.
|
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
|
||||||
Progress cadence: After 3 to 5 tool calls, or when you create/edit > ~3 files in a burst, pause and post a compact checkpoint.
|
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
|
||||||
Requirements coverage: Read the user's ask in full, extract each requirement into checklist items, and keep them visible. Do not omit a requirement. If something cannot be done with available tools, note why briefly and propose a viable alternative.
|
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
|
||||||
Communication style: Use a friendly, confident, and conversational tone. Prefer short sentences, contractions, and concrete language. Keep it skimmable and encouraging, not formal or robotic. A tiny touch of personality is okay; avoid overusing exclamations or emoji. Avoid empty filler like "Sounds good!", "Great!", "Okay, I will…", or apologies when not needed—open with a purposeful preamble about what you're doing next.
|
|
||||||
You will be given some context and attachments along with the user prompt. You can use them if they are relevant to the task, and ignore them if not.
|
You will be given some context and attachments along with the user prompt. You can use them if they are relevant to the task, and ignore them if not.
|
||||||
If you can infer the project type (languages, frameworks, and libraries) from the user's query or the context that you have, make sure to keep them in mind when making changes.
|
If you can infer the project type (languages, frameworks, and libraries) from the user's query or the context that you have, make sure to keep them in mind when making changes.
|
||||||
If you aren't sure which tool is relevant, you can call multiple tools. You can call tools repeatedly to take actions or gather as much context as needed until you have completed the task fully. Don't give up unless you are sure the request cannot be fulfilled with the tools you have. It's YOUR RESPONSIBILITY to make sure that you have done all you can to collect necessary context.
|
Use multiple tools as needed, and do not give up until the task is complete or impossible.
|
||||||
Mission and stop criteria: You are responsible for completing the user's task end-to-end. Continue working until the goal is satisfied or you are truly blocked by missing information. Do not defer actions back to the user if you can execute them yourself with available tools. Only ask a clarifying question when essential to proceed.
|
NEVER print codeblocks for file changes or terminal commands unless explicitly requested - use the appropriate tool.
|
||||||
Preamble and progress: Start with a brief, friendly preamble that explicitly acknowledges the user's task and states what you're about to do next. Make it engaging and tailored to the repo/task; keep it to a single sentence. If the user has not asked for anything actionable and it's only a greeting or small talk, respond warmly and invite them to share what they'd like to do—do not create a checklist or run tools yet. Use the preamble only once per task; if the previous assistant message already included a preamble for this task, skip it this turn. Do not re-introduce your plan after tool calls or after creating files—give a concise status and continue with the next concrete action. For multi-step tasks, keep a lightweight checklist and weave progress updates into your narration. Batch independent, read-only operations together; after a batch, share a concise progress note and what's next. If you say you will do something, execute it in the same turn using tools.
|
Do not repeat yourself after tool calls; continue from where you left off.
|
||||||
<requirementsUnderstanding>
|
You must use webfetch tool to recursively gather all information from URL's provided to you by the user, as well as any links you find in the content of those pages.
|
||||||
Always read the user's request in full before acting. Extract the explicit requirements and any reasonable implicit requirements.
|
</gptAgentInstructions>
|
||||||
If a requirement cannot be completed with available tools, state why briefly and propose a viable alternative or follow-up.
|
<structuredWorkflow>
|
||||||
|
# Workflow
|
||||||
|
1. Understand the problem deeply. Carefully read the issue and think critically about what is required.
|
||||||
|
2. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
|
||||||
|
3. Develop a clear, step-by-step plan. Break down the fix into manageable,
|
||||||
|
incremental steps - use the todo tool to track your progress.
|
||||||
|
4. Implement the fix incrementally. Make small, testable code changes.
|
||||||
|
5. Debug as needed. Use debugging techniques to isolate and resolve issues.
|
||||||
|
6. Test frequently. Run tests after each change to verify correctness.
|
||||||
|
7. Iterate until the root cause is fixed and all tests pass.
|
||||||
|
8. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
|
||||||
|
**CRITICAL - Before ending your turn:**
|
||||||
|
- Review and update the todo list, marking completed, skipped (with explanations), or blocked items.
|
||||||
|
|
||||||
</requirementsUnderstanding>
|
## 1. Deeply Understand the Problem
|
||||||
When reading files, prefer reading large meaningful chunks rather than consecutive small sections to minimize tool calls and gain better context.
|
- Carefully read the issue and think hard about a plan to solve it before coding.
|
||||||
Don't make assumptions about the situation- gather context first, then perform the task or answer the question.
|
- Break down the problem into manageable parts. Consider the following:
|
||||||
Under-specification policy: If details are missing, infer 1-2 reasonable assumptions from the repository conventions and proceed. Note assumptions briefly and continue; ask only when truly blocked.
|
- What is the expected behavior?
|
||||||
Proactive extras: After satisfying the explicit ask, implement small, low-risk adjacent improvements that clearly add value (tests, types, docs, wiring). If a follow-up is larger or risky, list it as next steps.
|
- What are the edge cases?
|
||||||
Anti-laziness: Avoid generic restatements and high-level advice. Prefer concrete edits, running tools, and verifying outcomes over suggesting what the user should do.
|
- What are the potential pitfalls?
|
||||||
<engineeringMindsetHints>
|
- How does this fit into the larger context of the codebase?
|
||||||
Think like a software engineer—when relevant, prefer to:
|
- What are the dependencies and interactions with other parts of the codee
|
||||||
- Outline a tiny “contract” in 2-4 bullets (inputs/outputs, data shapes, error modes, success criteria).
|
|
||||||
- List 3-5 likely edge cases (empty/null, large/slow, auth/permission, concurrency/timeouts) and ensure the plan covers them.
|
|
||||||
- Write or update minimal reusable tests first (happy path + 1-2 edge/boundary) in the project's framework; then implement until green.
|
|
||||||
|
|
||||||
</engineeringMindsetHints>
|
## 2. Codebase Investigation
|
||||||
<qualityGatesHints>
|
- Explore relevant files and directories.
|
||||||
Before wrapping up, prefer a quick “quality gates” triage: Build, Lint/Typecheck, Unit tests, and a small smoke test. Ensure there are no syntax/type errors across the project; fix them or clearly call out any intentionally deferred ones. Report deltas only (PASS/FAIL). Include a brief “requirements coverage” line mapping each requirement to its status (Done/Deferred + reason).
|
- Search for key functions, classes, or variables related to the issue.
|
||||||
|
- Read and understand relevant code snippets.
|
||||||
|
- Identify the root cause of the problem.
|
||||||
|
- Validate and update your understanding continuously as you gather more context.
|
||||||
|
|
||||||
</qualityGatesHints>
|
## 3. Develop a Detailed Plan
|
||||||
<responseModeHints>
|
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
|
||||||
Choose response mode based on task complexity. Prefer a lightweight answer when it's a greeting, small talk, or a trivial/direct Q&A that doesn't require tools or edits: keep it short, skip todo lists and progress checkpoints, and avoid tool calls unless necessary. Use the full engineering workflow (checklist, phases, checkpoints) when the task is multi-step, requires edits/builds/tests, or has ambiguity/unknowns. Escalate from light to full only when needed; if you escalate, say so briefly and continue.
|
- Create a todo list to track your progress.
|
||||||
|
- Each time you check off a step, update the todo list.
|
||||||
|
- Make sure that you ACTUALLY continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next.
|
||||||
|
|
||||||
</responseModeHints>
|
## 4. Making Code Changes
|
||||||
Validation and green-before-done: After any substantive change, run the relevant build/tests/linters automatically. For runnable code that you created or edited, immediately run a test to validate the code works (fast, minimal input) yourself using terminal tools. Prefer automated code-based tests where possible. Then provide optional fenced code blocks with commands for larger or platform-specific runs. Don't end a turn with a broken build if you can fix it. If failures occur, iterate up to three targeted fixes; if still failing, summarize the root cause, options, and exact failing output. For non-critical checks (e.g., a flaky health check), retry briefly (2-3 attempts with short backoff) and then proceed with the next step, noting the flake.
|
- Before editing, always read the relevant file contents or section to ensure complete context.
|
||||||
Never invent file paths, APIs, or commands. Verify with tools (search/read/list) before acting when uncertain.
|
- Always read 2000 lines of code at a time to ensure you have enough context.
|
||||||
Security and side-effects: Do not exfiltrate secrets or make network calls unless explicitly required by the task. Prefer local actions first.
|
- If a patch is not applied correctly, attempt to reapply it.
|
||||||
Reproducibility and dependencies: Follow the project's package manager and configuration; prefer minimal, pinned, widely-used libraries and update manifests or lockfiles appropriately. Prefer adding or updating tests when you change public behavior.
|
- Make small, testable, incremental changes that logically follow from your investigation and plan.
|
||||||
Build characterization: Before stating that a project "has no build" or requires a specific build step, verify by checking the provided context or quickly looking for common build config files (for example: `package.json`, `pnpm-lock.yaml`, `requirements.txt`, `pyproject.toml`, `setup.py`, `Makefile`, `Dockerfile`, `build.gradle`, `pom.xml`). If uncertain, say what you know based on the available evidence and proceed with minimal setup instructions; note that you can adapt if additional build configs exist.
|
- Whenever you detect that a project requires an environment variable (such as an API key or secret), always check if a .env file exists in the project root. If it does not exist, automatically create a .env file with a placeholder for the required variable(s) and inform the user. Do this proactively, without waiting for the user to request it.
|
||||||
Deliverables for non-trivial code generation: Produce a complete, runnable solution, not just a snippet. Create the necessary source files plus a small runner or test/benchmark harness when relevant, a minimal `README.md` with usage and troubleshooting, and a dependency manifest (for example, `package.json`, `requirements.txt`, `pyproject.toml`) updated or added as appropriate. If you intentionally choose not to create one of these artifacts, briefly say why.
|
|
||||||
Don't repeat yourself after a tool call, pick up where you left off.
|
## 5. Debugging
|
||||||
You don't need to read a file if it's already provided in context.
|
- Make code changes only if you have high confidence they can solve the problem
|
||||||
</instructions>
|
- When debugging, try to determine the root cause rather than addressing symptoms
|
||||||
<toolUseInstructions>
|
- Debug for as long as needed to identify the root cause and identify a fix
|
||||||
If the user is requesting a code sample, you can answer it directly without using any tools.
|
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening
|
||||||
When using a tool, follow the JSON schema very carefully and make sure to include ALL required properties.
|
- To test hypotheses, you can also add test statements or functions
|
||||||
No need to ask permission before using a tool.
|
- Revisit your assumptions if unexpected behavior occurs.
|
||||||
NEVER say the name of a tool to a user. For example, instead of saying that you'll use the run_in_terminal tool, say "I'll run the command in a terminal".
|
|
||||||
If you think running multiple tools can answer the user's question, prefer calling them in parallel whenever possible, but do not call semantic_search in parallel.
|
</structuredWorkflow>
|
||||||
Before notable tool batches, briefly tell the user what you're about to do and why. After the results return, briefly interpret them and state what you'll do next. Don't narrate every trivial call.
|
<communicationGuidelines>
|
||||||
You MUST preface each tool call batch with a one-sentence “why/what/outcome” preamble (why you're doing it, what you'll run, expected outcome). If you make many tool calls in a row, you MUST checkpoint progress after roughly every 3-5 calls: what you ran, key results, and what you'll do next. If you create or edit more than ~3 files in a burst, checkpoint immediately with a compact bullet summary.
|
Always communicate clearly and concisely in a warm and friendly yet professional tone. Use upbeat language and sprinkle in light, witty humor where appropriate.
|
||||||
If you think running multiple tools can answer the user's question, prefer calling them in parallel whenever possible, but do not call semantic_search in parallel. Parallelize read-only, independent operations only; do not parallelize edits or dependent steps.
|
If the user corrects you, do not immediately assume they are right. Think deeply about their feedback and how you can incorporate it into your solution. Stand your ground if you have the evidence to support your conclusion.
|
||||||
Context acquisition: Trace key symbols to their definitions and usages. Read sufficiently large, meaningful chunks to avoid missing context. Prefer semantic or codebase search when you don't know the exact string; prefer exact search or direct reads when you do. Avoid redundant reads when the content is already attached and sufficient.
|
|
||||||
Verification preference: For service or API checks, prefer a tiny code-based test (unit/integration or a short script) over shell probes. Use shell probes (e.g., curl) only as optional documentation or quick one-off sanity checks, and mark them as optional.
|
</communicationGuidelines>
|
||||||
If semantic_search returns the full contents of the text files in the workspace, you have all the workspace context.
|
|
||||||
You can use the grep_search to get an overview of a file by searching for a string within that one file, instead of using read_file many times.
|
|
||||||
If you don't know exactly the string or filename pattern you're looking for, use semantic_search to do a semantic search across the workspace.
|
|
||||||
When invoking a tool that takes a file path, always use the absolute file path. If the file has a scheme like untitled: or vscode-userdata:, then use a URI with the scheme.
|
|
||||||
You don't currently have any tools available for editing files. If the user asks you to edit a file, you can ask the user to enable editing tools or print a codeblock with the suggested changes.
|
|
||||||
You don't currently have any tools available for running terminal commands. If the user asks you to run a terminal command, you can ask the user to enable terminal tools or print a codeblock with the suggested command.
|
|
||||||
Tools can be disabled by the user. You may see tools used previously in the conversation that are not currently available. Be careful to only use the tools that are currently available to you.
|
|
||||||
</toolUseInstructions>
|
|
||||||
<codeSearchInstructions>
|
<codeSearchInstructions>
|
||||||
These instructions only apply when the question is about the user's workspace.
|
These instructions only apply when the question is about the user's workspace.
|
||||||
First, analyze the developer's request to determine how complicated their task is. Leverage any of the tools available to you to gather the context needed to provided a complete and accurate response. Keep your search focused on the developer's request, and don't run extra tools if the developer's request clearly can be satisfied by just one.
|
First, analyze the developer's request to determine how complicated their task is. Leverage any of the tools available to you to gather the context needed to provided a complete and accurate response. Keep your search focused on the developer's request, and don't run extra tools if the developer's request clearly can be satisfied by just one.
|
||||||
@@ -109,6 +113,19 @@ In the code block, use a line comment with '...existing code...' to indicate cod
|
|||||||
{ changed code }
|
{ changed code }
|
||||||
// ...existing code...
|
// ...existing code...
|
||||||
````
|
````
|
||||||
|
<toolUseInstructions>
|
||||||
|
If the user is requesting a code sample, you can answer it directly without using any tools.
|
||||||
|
When using a tool, follow the JSON schema very carefully and make sure to include ALL required properties.
|
||||||
|
No need to ask permission before using a tool.
|
||||||
|
NEVER say the name of a tool to a user. For example, instead of saying that you'll use the run_in_terminal tool, say "I'll run the command in a terminal".
|
||||||
|
If you think running multiple tools can answer the user's question, prefer calling them in parallel whenever possible, but do not call semantic_search in parallel.
|
||||||
|
If semantic_search returns the full contents of the text files in the workspace, you have all the workspace context.
|
||||||
|
You can use the grep_search to get an overview of a file by searching for a string within that one file, instead of using read_file many times.
|
||||||
|
If you don't know exactly the string or filename pattern you're looking for, use semantic_search to do a semantic search across the workspace.
|
||||||
|
When invoking a tool that takes a file path, always use the absolute file path.
|
||||||
|
Tools can be disabled by the user. You may see tools used previously in the conversation that are not currently available. Be careful to only use the tools that are currently available to you.
|
||||||
|
</toolUseInstructions>
|
||||||
|
|
||||||
<outputFormatting>
|
<outputFormatting>
|
||||||
Use proper Markdown formatting in your answers. When referring to a filename or symbol in the user's workspace, wrap it in backticks.
|
Use proper Markdown formatting in your answers. When referring to a filename or symbol in the user's workspace, wrap it in backticks.
|
||||||
When sharing setup or run steps for the user to execute, render commands in fenced code blocks with an appropriate language tag (`bash`, `sh`, `powershell`, `python`, etc.). Keep one command per line; avoid prose-only representations of commands.
|
When sharing setup or run steps for the user to execute, render commands in fenced code blocks with an appropriate language tag (`bash`, `sh`, `powershell`, `python`, etc.). Keep one command per line; avoid prose-only representations of commands.
|
||||||
|
|||||||
@@ -5,25 +5,42 @@ import { Tool } from "./tool"
|
|||||||
import DESCRIPTION from "./bash.txt"
|
import DESCRIPTION from "./bash.txt"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
import { Config } from "../config/config"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Wildcard } from "../util/wildcard"
|
import { Wildcard } from "../util/wildcard"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
import { Agent } from "../agent/agent"
|
||||||
|
|
||||||
const MAX_OUTPUT_LENGTH = 30000
|
const MAX_OUTPUT_LENGTH = 30_000
|
||||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||||
|
|
||||||
const log = Log.create({ service: "bash-tool" })
|
const log = Log.create({ service: "bash-tool" })
|
||||||
|
|
||||||
const parser = lazy(async () => {
|
const parser = lazy(async () => {
|
||||||
const { default: Parser } = await import("tree-sitter")
|
try {
|
||||||
const Bash = await import("tree-sitter-bash")
|
const { default: Parser } = await import("tree-sitter")
|
||||||
const p = new Parser()
|
const Bash = await import("tree-sitter-bash")
|
||||||
p.setLanguage(Bash.language as any)
|
const p = new Parser()
|
||||||
return p
|
p.setLanguage(Bash.language as any)
|
||||||
|
return p
|
||||||
|
} catch (e) {
|
||||||
|
const { default: Parser } = await import("web-tree-sitter")
|
||||||
|
const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { with: { type: "wasm" } })
|
||||||
|
await Parser.init({
|
||||||
|
locateFile() {
|
||||||
|
return treeWasm
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, {
|
||||||
|
with: { type: "wasm" },
|
||||||
|
})
|
||||||
|
const bashLanguage = await Parser.Language.load(bashWasm)
|
||||||
|
const p = new Parser()
|
||||||
|
p.setLanguage(bashLanguage)
|
||||||
|
return p
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const BashTool = Tool.define("bash", {
|
export const BashTool = Tool.define("bash", {
|
||||||
@@ -40,20 +57,8 @@ export const BashTool = Tool.define("bash", {
|
|||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
const cfg = await Config.get()
|
|
||||||
const tree = await parser().then((p) => p.parse(params.command))
|
const tree = await parser().then((p) => p.parse(params.command))
|
||||||
const permissions = (() => {
|
const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
|
||||||
const value = cfg.permission?.bash
|
|
||||||
if (!value)
|
|
||||||
return {
|
|
||||||
"*": "allow",
|
|
||||||
}
|
|
||||||
if (typeof value === "string")
|
|
||||||
return {
|
|
||||||
"*": value,
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
})()
|
|
||||||
|
|
||||||
let needsAsk = false
|
let needsAsk = false
|
||||||
for (const node of tree.rootNode.descendantsOfType("command")) {
|
for (const node of tree.rootNode.descendantsOfType("command")) {
|
||||||
@@ -93,17 +98,10 @@ export const BashTool = Tool.define("bash", {
|
|||||||
|
|
||||||
// always allow cd if it passes above check
|
// always allow cd if it passes above check
|
||||||
if (!needsAsk && command[0] !== "cd") {
|
if (!needsAsk && command[0] !== "cd") {
|
||||||
const action = (() => {
|
const action = Wildcard.all(node.text, permissions)
|
||||||
for (const [pattern, value] of Object.entries(permissions)) {
|
|
||||||
const match = Wildcard.match(node.text, pattern)
|
|
||||||
log.info("checking", { text: node.text.trim(), pattern, match })
|
|
||||||
if (match) return value
|
|
||||||
}
|
|
||||||
return "ask"
|
|
||||||
})()
|
|
||||||
if (action === "deny") {
|
if (action === "deny") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"The user has specifically restricted access to this command, you are not allowed to execute it.",
|
`The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (action === "ask") needsAsk = true
|
if (action === "ask") needsAsk = true
|
||||||
@@ -113,6 +111,7 @@ export const BashTool = Tool.define("bash", {
|
|||||||
if (needsAsk) {
|
if (needsAsk) {
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "bash",
|
type: "bash",
|
||||||
|
pattern: params.command,
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
messageID: ctx.messageID,
|
messageID: ctx.messageID,
|
||||||
callID: ctx.callID,
|
callID: ctx.callID,
|
||||||
@@ -126,7 +125,6 @@ export const BashTool = Tool.define("bash", {
|
|||||||
const process = exec(params.command, {
|
const process = exec(params.command, {
|
||||||
cwd: app.path.cwd,
|
cwd: app.path.cwd,
|
||||||
signal: ctx.abort,
|
signal: ctx.abort,
|
||||||
maxBuffer: MAX_OUTPUT_LENGTH,
|
|
||||||
timeout,
|
timeout,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -174,6 +172,11 @@ export const BashTool = Tool.define("bash", {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (output.length > MAX_OUTPUT_LENGTH) {
|
||||||
|
output = output.slice(0, MAX_OUTPUT_LENGTH)
|
||||||
|
output += "\n\n(Output was truncated due to length limit)"
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: params.command,
|
title: params.command,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { App } from "../app/app"
|
|||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { FileTime } from "../file/time"
|
import { FileTime } from "../file/time"
|
||||||
import { Config } from "../config/config"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
import { Agent } from "../agent/agent"
|
||||||
|
|
||||||
export const EditTool = Tool.define("edit", {
|
export const EditTool = Tool.define("edit", {
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
@@ -40,7 +40,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
throw new Error(`File ${filePath} is not in the current working directory`)
|
throw new Error(`File ${filePath} is not in the current working directory`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg = await Config.get()
|
const agent = await Agent.get(ctx.agent)
|
||||||
let diff = ""
|
let diff = ""
|
||||||
let contentOld = ""
|
let contentOld = ""
|
||||||
let contentNew = ""
|
let contentNew = ""
|
||||||
@@ -48,7 +48,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
if (params.oldString === "") {
|
if (params.oldString === "") {
|
||||||
contentNew = params.newString
|
contentNew = params.newString
|
||||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||||
if (cfg.permission?.edit === "ask") {
|
if (agent.permission.edit === "ask") {
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "edit",
|
type: "edit",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
@@ -77,12 +77,13 @@ export const EditTool = Tool.define("edit", {
|
|||||||
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
||||||
|
|
||||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||||
if (cfg.permission?.edit === "ask") {
|
if (agent.permission.edit === "ask") {
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "edit",
|
type: "edit",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
messageID: ctx.messageID,
|
messageID: ctx.messageID,
|
||||||
callID: ctx.callID,
|
callID: ctx.callID,
|
||||||
|
pattern: filePath,
|
||||||
title: "Edit this file: " + filePath,
|
title: "Edit this file: " + filePath,
|
||||||
metadata: {
|
metadata: {
|
||||||
filePath,
|
filePath,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const ReadTool = Tool.define("read", {
|
|||||||
filepath = path.join(process.cwd(), filepath)
|
filepath = path.join(process.cwd(), filepath)
|
||||||
}
|
}
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
if (!Filesystem.contains(app.path.cwd, filepath)) {
|
if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(app.path.cwd, filepath)) {
|
||||||
throw new Error(`File ${filepath} is not in the current working directory`)
|
throw new Error(`File ${filepath} is not in the current working directory`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +98,6 @@ function isImageFile(filePath: string): string | false {
|
|||||||
return "GIF"
|
return "GIF"
|
||||||
case ".bmp":
|
case ".bmp":
|
||||||
return "BMP"
|
return "BMP"
|
||||||
case ".svg":
|
|
||||||
return "SVG"
|
|
||||||
case ".webp":
|
case ".webp":
|
||||||
return "WebP"
|
return "WebP"
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Usage:
|
|||||||
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
||||||
- Any lines longer than 2000 characters will be truncated
|
- Any lines longer than 2000 characters will be truncated
|
||||||
- Results are returned using cat -n format, with line numbers starting at 1
|
- Results are returned using cat -n format, with line numbers starting at 1
|
||||||
- This tool allows opencode to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as opencode is a multimodal LLM.
|
- This tool cannot read binary files, including images
|
||||||
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
||||||
- You will regularly be asked to read screenshots. If the user provides a path to a screenshot ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths like /var/folders/123/abc/T/TemporaryItems/NSIRD_screencaptureui_ZfB1tD/Screenshot.png
|
|
||||||
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { TodoWriteTool, TodoReadTool } from "./todo"
|
|||||||
import { WebFetchTool } from "./webfetch"
|
import { WebFetchTool } from "./webfetch"
|
||||||
import { WriteTool } from "./write"
|
import { WriteTool } from "./write"
|
||||||
import { InvalidTool } from "./invalid"
|
import { InvalidTool } from "./invalid"
|
||||||
import { Config } from "../config/config"
|
import type { Agent } from "../agent/agent"
|
||||||
|
|
||||||
export namespace ToolRegistry {
|
export namespace ToolRegistry {
|
||||||
const ALL = [
|
const ALL = [
|
||||||
@@ -66,23 +66,31 @@ export namespace ToolRegistry {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enabled(_providerID: string, _modelID: string): Promise<Record<string, boolean>> {
|
export async function enabled(
|
||||||
const cfg = await Config.get()
|
_providerID: string,
|
||||||
|
modelID: string,
|
||||||
|
agent: Agent.Info,
|
||||||
|
): Promise<Record<string, boolean>> {
|
||||||
const result: Record<string, boolean> = {}
|
const result: Record<string, boolean> = {}
|
||||||
result["patch"] = false
|
result["patch"] = false
|
||||||
|
|
||||||
if (cfg.permission?.edit === "deny") {
|
if (agent.permission.edit === "deny") {
|
||||||
result["edit"] = false
|
result["edit"] = false
|
||||||
result["patch"] = false
|
result["patch"] = false
|
||||||
result["write"] = false
|
result["write"] = false
|
||||||
}
|
}
|
||||||
if (cfg?.permission?.bash === "deny") {
|
if (agent.permission.bash["*"] === "deny" && Object.keys(agent.permission.bash).length === 1) {
|
||||||
result["bash"] = false
|
result["bash"] = false
|
||||||
}
|
}
|
||||||
if (cfg?.permission?.webfetch === "deny") {
|
if (agent.permission.webfetch === "deny") {
|
||||||
result["webfetch"] = false
|
result["webfetch"] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (modelID.includes("qwen")) {
|
||||||
|
result["todowrite"] = false
|
||||||
|
result["todoread"] = false
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ export const TaskTool = Tool.define("task", async () => {
|
|||||||
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
|
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
|
||||||
}),
|
}),
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
const session = await Session.create(ctx.sessionID)
|
|
||||||
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
|
|
||||||
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
|
|
||||||
const agent = await Agent.get(params.subagent_type)
|
const agent = await Agent.get(params.subagent_type)
|
||||||
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
|
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
|
||||||
|
const session = await Session.create(ctx.sessionID, params.description + ` (@${agent.name} subagent)`)
|
||||||
|
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
|
||||||
|
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
|
||||||
const messageID = Identifier.ascending("message")
|
const messageID = Identifier.ascending("message")
|
||||||
const parts: Record<string, MessageV2.ToolPart> = {}
|
const parts: Record<string, MessageV2.ToolPart> = {}
|
||||||
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
|
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Launch a new agent to handle complex, multi-step tasks autonomously.
|
Launch a new agent to handle complex, multi-step tasks autonomously.
|
||||||
|
|
||||||
Available agent types and the tools they have access to:
|
Available agent types and the tools they have access to:
|
||||||
{agents}
|
{agents}
|
||||||
@@ -23,7 +23,7 @@ Usage notes:
|
|||||||
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
|
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
|
||||||
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
||||||
|
|
||||||
Example usage:
|
Example usage (NOTE: The agents below are fictional examples for illustration only - use the actual agents listed above):
|
||||||
|
|
||||||
<example_agent_descriptions>
|
<example_agent_descriptions>
|
||||||
"code-reviewer": use this agent after you are done writing a signficant piece of code
|
"code-reviewer": use this agent after you are done writing a signficant piece of code
|
||||||
@@ -48,7 +48,7 @@ function isPrime(n) {
|
|||||||
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
|
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
|
||||||
</commentary>
|
</commentary>
|
||||||
assistant: Now let me use the code-reviewer agent to review the code
|
assistant: Now let me use the code-reviewer agent to review the code
|
||||||
assistant: Uses the Task tool to launch the with the code-reviewer agent
|
assistant: Uses the Task tool to launch the code-reviewer agent
|
||||||
</example>
|
</example>
|
||||||
|
|
||||||
<example>
|
<example>
|
||||||
|
|||||||
@@ -1,53 +1,70 @@
|
|||||||
import Parser from "tree-sitter";
|
const parser = async () => {
|
||||||
import Bash from "tree-sitter-bash";
|
try {
|
||||||
|
const { default: Parser } = await import("tree-sitter")
|
||||||
|
const Bash = await import("tree-sitter-bash")
|
||||||
|
const p = new Parser()
|
||||||
|
p.setLanguage(Bash.language as any)
|
||||||
|
return p
|
||||||
|
} catch (e) {
|
||||||
|
const { default: Parser } = await import("web-tree-sitter")
|
||||||
|
const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { with: { type: "wasm" } })
|
||||||
|
await Parser.init({
|
||||||
|
locateFile() {
|
||||||
|
return treeWasm
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, {
|
||||||
|
with: { type: "wasm" },
|
||||||
|
})
|
||||||
|
const bashLanguage = await Parser.Language.load(bashWasm)
|
||||||
|
const p = new Parser()
|
||||||
|
p.setLanguage(bashLanguage)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const parser = new Parser();
|
const sourceCode = `cd --foo foo/bar && echo "hello" && cd ../baz`
|
||||||
parser.setLanguage(Bash.language as any);
|
|
||||||
|
|
||||||
const sourceCode = `cd --foo foo/bar && echo "hello" && cd ../baz`;
|
const tree = await parser().then((p) => p.parse(sourceCode))
|
||||||
|
|
||||||
const tree = parser.parse(sourceCode);
|
|
||||||
|
|
||||||
// Function to extract commands and arguments
|
// Function to extract commands and arguments
|
||||||
function extractCommands(
|
function extractCommands(node: any): Array<{ command: string; args: string[] }> {
|
||||||
node: any,
|
const commands: Array<{ command: string; args: string[] }> = []
|
||||||
): Array<{ command: string; args: string[] }> {
|
|
||||||
const commands: Array<{ command: string; args: string[] }> = [];
|
|
||||||
|
|
||||||
function traverse(node: any) {
|
function traverse(node: any) {
|
||||||
if (node.type === "command") {
|
if (node.type === "command") {
|
||||||
const commandNode = node.child(0);
|
const commandNode = node.child(0)
|
||||||
if (commandNode) {
|
if (commandNode) {
|
||||||
const command = commandNode.text;
|
const command = commandNode.text
|
||||||
const args: string[] = [];
|
const args: string[] = []
|
||||||
|
|
||||||
// Extract arguments
|
// Extract arguments
|
||||||
for (let i = 1; i < node.childCount; i++) {
|
for (let i = 1; i < node.childCount; i++) {
|
||||||
const child = node.child(i);
|
const child = node.child(i)
|
||||||
if (child && child.type === "word") {
|
if (child && child.type === "word") {
|
||||||
args.push(child.text);
|
args.push(child.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.push({ command, args });
|
commands.push({ command, args })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traverse children
|
// Traverse children
|
||||||
for (let i = 0; i < node.childCount; i++) {
|
for (let i = 0; i < node.childCount; i++) {
|
||||||
traverse(node.child(i));
|
traverse(node.child(i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
traverse(node);
|
traverse(node)
|
||||||
return commands;
|
return commands
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and display commands
|
// Extract and display commands
|
||||||
console.log("Source code: " + sourceCode);
|
console.log("Source code: " + sourceCode)
|
||||||
const commands = extractCommands(tree.rootNode);
|
const commands = extractCommands(tree.rootNode)
|
||||||
console.log("Extracted commands:");
|
console.log("Extracted commands:")
|
||||||
commands.forEach((cmd, index) => {
|
commands.forEach((cmd, index) => {
|
||||||
console.log(`${index + 1}. Command: ${cmd.command}`);
|
console.log(`${index + 1}. Command: ${cmd.command}`)
|
||||||
console.log(` Args: [${cmd.args.join(", ")}]`);
|
console.log(` Args: [${cmd.args.join(", ")}]`)
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ export namespace Tool {
|
|||||||
export type Context<M extends Metadata = Metadata> = {
|
export type Context<M extends Metadata = Metadata> = {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
|
agent: string
|
||||||
callID?: string
|
callID?: string
|
||||||
abort: AbortSignal
|
abort: AbortSignal
|
||||||
|
extra?: { [key: string]: any }
|
||||||
metadata(input: { title?: string; metadata?: M }): void
|
metadata(input: { title?: string; metadata?: M }): void
|
||||||
}
|
}
|
||||||
export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
|
export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const WebFetchTool = Tool.define("webfetch", {
|
|||||||
if (cfg.permission?.webfetch === "ask")
|
if (cfg.permission?.webfetch === "ask")
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "webfetch",
|
type: "webfetch",
|
||||||
|
pattern: params.url,
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
messageID: ctx.messageID,
|
messageID: ctx.messageID,
|
||||||
callID: ctx.callID,
|
callID: ctx.callID,
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { App } from "../app/app"
|
|||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
import { FileTime } from "../file/time"
|
import { FileTime } from "../file/time"
|
||||||
import { Config } from "../config/config"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
import { Agent } from "../agent/agent"
|
||||||
|
|
||||||
export const WriteTool = Tool.define("write", {
|
export const WriteTool = Tool.define("write", {
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
@@ -28,8 +28,8 @@ export const WriteTool = Tool.define("write", {
|
|||||||
const exists = await file.exists()
|
const exists = await file.exists()
|
||||||
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
||||||
|
|
||||||
const cfg = await Config.get()
|
const agent = await Agent.get(ctx.agent)
|
||||||
if (cfg.permission?.edit === "ask")
|
if (agent.permission.edit === "ask")
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "write",
|
type: "write",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export function defer<T extends () => void | Promise<void>>(
|
||||||
|
fn: T,
|
||||||
|
): T extends () => Promise<void> ? { [Symbol.asyncDispose]: () => Promise<void> } : { [Symbol.dispose]: () => void } {
|
||||||
|
return {
|
||||||
|
[Symbol.dispose]() {
|
||||||
|
fn()
|
||||||
|
},
|
||||||
|
[Symbol.asyncDispose]() {
|
||||||
|
return Promise.resolve(fn())
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { sortBy, pipe } from "remeda"
|
||||||
|
|
||||||
export namespace Wildcard {
|
export namespace Wildcard {
|
||||||
export function match(str: string, pattern: string) {
|
export function match(str: string, pattern: string) {
|
||||||
const regex = new RegExp(
|
const regex = new RegExp(
|
||||||
@@ -11,4 +13,16 @@ export namespace Wildcard {
|
|||||||
)
|
)
|
||||||
return regex.test(str)
|
return regex.test(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function all(input: string, patterns: Record<string, any>) {
|
||||||
|
const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]))
|
||||||
|
let result = undefined
|
||||||
|
for (const [pattern, value] of sorted) {
|
||||||
|
if (match(input, pattern)) {
|
||||||
|
result = value
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const ctx = {
|
|||||||
sessionID: "test",
|
sessionID: "test",
|
||||||
messageID: "",
|
messageID: "",
|
||||||
toolCallID: "",
|
toolCallID: "",
|
||||||
|
agent: "build",
|
||||||
abort: AbortSignal.any([]),
|
abort: AbortSignal.any([]),
|
||||||
metadata: () => {},
|
metadata: () => {},
|
||||||
}
|
}
|
||||||
@@ -33,7 +34,7 @@ describe("tool.bash", () => {
|
|||||||
|
|
||||||
test("cd ../ should fail outside of project root", async () => {
|
test("cd ../ should fail outside of project root", async () => {
|
||||||
await App.provide({ cwd: projectRoot }, async () => {
|
await App.provide({ cwd: projectRoot }, async () => {
|
||||||
await expect(
|
expect(
|
||||||
bash.execute(
|
bash.execute(
|
||||||
{
|
{
|
||||||
command: "cd ../",
|
command: "cd ../",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const ctx = {
|
|||||||
sessionID: "test",
|
sessionID: "test",
|
||||||
messageID: "",
|
messageID: "",
|
||||||
toolCallID: "",
|
toolCallID: "",
|
||||||
|
agent: "build",
|
||||||
abort: AbortSignal.any([]),
|
abort: AbortSignal.any([]),
|
||||||
metadata: () => {},
|
metadata: () => {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "0.4.26",
|
"version": "0.5.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import type { Event, createOpencodeClient, App, Model, Provider, Permission, UserMessage, Part } from "@opencode-ai/sdk"
|
import type {
|
||||||
|
Event,
|
||||||
|
createOpencodeClient,
|
||||||
|
App,
|
||||||
|
Model,
|
||||||
|
Provider,
|
||||||
|
Permission,
|
||||||
|
UserMessage,
|
||||||
|
Part,
|
||||||
|
Auth,
|
||||||
|
} from "@opencode-ai/sdk"
|
||||||
import type { BunShell } from "./shell"
|
import type { BunShell } from "./shell"
|
||||||
|
|
||||||
export type PluginInput = {
|
export type PluginInput = {
|
||||||
@@ -10,6 +20,49 @@ export type Plugin = (input: PluginInput) => Promise<Hooks>
|
|||||||
|
|
||||||
export interface Hooks {
|
export interface Hooks {
|
||||||
event?: (input: { event: Event }) => Promise<void>
|
event?: (input: { event: Event }) => Promise<void>
|
||||||
|
auth?: {
|
||||||
|
provider: string
|
||||||
|
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
|
||||||
|
methods: (
|
||||||
|
| {
|
||||||
|
type: "oauth"
|
||||||
|
label: string
|
||||||
|
authorize(): Promise<
|
||||||
|
{ url: string; instructions: string } & (
|
||||||
|
| {
|
||||||
|
method: "auto"
|
||||||
|
callback(): Promise<
|
||||||
|
| {
|
||||||
|
type: "success"
|
||||||
|
refresh: string
|
||||||
|
access: string
|
||||||
|
expires: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "failed"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: "code"
|
||||||
|
callback(code: string): Promise<
|
||||||
|
| {
|
||||||
|
type: "success"
|
||||||
|
refresh: string
|
||||||
|
access: string
|
||||||
|
expires: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "failed"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
>
|
||||||
|
}
|
||||||
|
| { type: "api"; label: string }
|
||||||
|
)[]
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Called when a new message is received
|
* Called when a new message is received
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
configured_endpoints: 34
|
configured_endpoints: 39
|
||||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-b86cf7bb8df4f60ebe8b8f51e281c8076cfdccc8554178c1b78beca4b025f0ff.yml
|
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-e4b6496e5f2c68fa8b3ea1b88e40041eaf5ce2652001344df80bf130675d1766.yml
|
||||||
openapi_spec_hash: 47633b7481d91708643ea7b43fffffe6
|
openapi_spec_hash: df474311dc9e4a89cd483bd8b8d971d8
|
||||||
config_hash: bd7f6435ed0c0005f373b5526c07a055
|
config_hash: eab3723c4c2232a6ba1821151259d6da
|
||||||
|
|||||||
@@ -110,16 +110,20 @@ Response Types:
|
|||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
|
|
||||||
- <code title="post /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.New">New</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.New">New</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionNewParams">SessionNewParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
- <code title="patch /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Update">Update</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionUpdateParams">SessionUpdateParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
- <code title="get /session/{id}/children">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Children">Children</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
- <code title="get /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /session/{id}/revert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Revert">Revert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionRevertParams">SessionRevertParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /session/{id}/revert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Revert">Revert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionRevertParams">SessionRevertParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
- <code title="post /session/{id}/shell">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Shell">Shell</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionShellParams">SessionShellParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /session/{id}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionSummarizeParams">SessionSummarizeParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /session/{id}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionSummarizeParams">SessionSummarizeParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /session/{id}/unrevert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unrevert">Unrevert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /session/{id}/unrevert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unrevert">Unrevert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
@@ -145,4 +149,5 @@ Methods:
|
|||||||
- <code title="post /tui/open-models">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenModels">OpenModels</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /tui/open-models">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenModels">OpenModels</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /tui/open-sessions">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenSessions">OpenSessions</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /tui/open-sessions">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenSessions">OpenSessions</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /tui/open-themes">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenThemes">OpenThemes</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /tui/open-themes">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenThemes">OpenThemes</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
- <code title="post /tui/show-toast">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.ShowToast">ShowToast</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiShowToastParams">TuiShowToastParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /tui/submit-prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.SubmitPrompt">SubmitPrompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /tui/submit-prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.SubmitPrompt">SubmitPrompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
|||||||
+87
-9
@@ -72,21 +72,27 @@ func (r *AppService) Providers(ctx context.Context, opts ...option.RequestOption
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
Mode AgentMode `json:"mode,required"`
|
BuiltIn bool `json:"builtIn,required"`
|
||||||
Name string `json:"name,required"`
|
Mode AgentMode `json:"mode,required"`
|
||||||
Tools map[string]bool `json:"tools,required"`
|
Name string `json:"name,required"`
|
||||||
Description string `json:"description"`
|
Options map[string]interface{} `json:"options,required"`
|
||||||
Model AgentModel `json:"model"`
|
Permission AgentPermission `json:"permission,required"`
|
||||||
Prompt string `json:"prompt"`
|
Tools map[string]bool `json:"tools,required"`
|
||||||
Temperature float64 `json:"temperature"`
|
Description string `json:"description"`
|
||||||
TopP float64 `json:"topP"`
|
Model AgentModel `json:"model"`
|
||||||
JSON agentJSON `json:"-"`
|
Prompt string `json:"prompt"`
|
||||||
|
Temperature float64 `json:"temperature"`
|
||||||
|
TopP float64 `json:"topP"`
|
||||||
|
JSON agentJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// agentJSON contains the JSON metadata for the struct [Agent]
|
// agentJSON contains the JSON metadata for the struct [Agent]
|
||||||
type agentJSON struct {
|
type agentJSON struct {
|
||||||
|
BuiltIn apijson.Field
|
||||||
Mode apijson.Field
|
Mode apijson.Field
|
||||||
Name apijson.Field
|
Name apijson.Field
|
||||||
|
Options apijson.Field
|
||||||
|
Permission apijson.Field
|
||||||
Tools apijson.Field
|
Tools apijson.Field
|
||||||
Description apijson.Field
|
Description apijson.Field
|
||||||
Model apijson.Field
|
Model apijson.Field
|
||||||
@@ -121,6 +127,78 @@ func (r AgentMode) IsKnown() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentPermission struct {
|
||||||
|
Bash map[string]AgentPermissionBash `json:"bash,required"`
|
||||||
|
Edit AgentPermissionEdit `json:"edit,required"`
|
||||||
|
Webfetch AgentPermissionWebfetch `json:"webfetch"`
|
||||||
|
JSON agentPermissionJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// agentPermissionJSON contains the JSON metadata for the struct [AgentPermission]
|
||||||
|
type agentPermissionJSON struct {
|
||||||
|
Bash apijson.Field
|
||||||
|
Edit apijson.Field
|
||||||
|
Webfetch apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AgentPermission) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r agentPermissionJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPermissionBash string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AgentPermissionBashAsk AgentPermissionBash = "ask"
|
||||||
|
AgentPermissionBashAllow AgentPermissionBash = "allow"
|
||||||
|
AgentPermissionBashDeny AgentPermissionBash = "deny"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r AgentPermissionBash) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case AgentPermissionBashAsk, AgentPermissionBashAllow, AgentPermissionBashDeny:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPermissionEdit string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AgentPermissionEditAsk AgentPermissionEdit = "ask"
|
||||||
|
AgentPermissionEditAllow AgentPermissionEdit = "allow"
|
||||||
|
AgentPermissionEditDeny AgentPermissionEdit = "deny"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r AgentPermissionEdit) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case AgentPermissionEditAsk, AgentPermissionEditAllow, AgentPermissionEditDeny:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPermissionWebfetch string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AgentPermissionWebfetchAsk AgentPermissionWebfetch = "ask"
|
||||||
|
AgentPermissionWebfetchAllow AgentPermissionWebfetch = "allow"
|
||||||
|
AgentPermissionWebfetchDeny AgentPermissionWebfetch = "deny"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r AgentPermissionWebfetch) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case AgentPermissionWebfetchAsk, AgentPermissionWebfetchAllow, AgentPermissionWebfetchDeny:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type AgentModel struct {
|
type AgentModel struct {
|
||||||
ModelID string `json:"modelID,required"`
|
ModelID string `json:"modelID,required"`
|
||||||
ProviderID string `json:"providerID,required"`
|
ProviderID string `json:"providerID,required"`
|
||||||
|
|||||||
+744
-100
File diff suppressed because it is too large
Load Diff
+121
-123
@@ -54,13 +54,13 @@ type EventListResponse struct {
|
|||||||
// [EventListResponseEventMessageRemovedProperties],
|
// [EventListResponseEventMessageRemovedProperties],
|
||||||
// [EventListResponseEventMessagePartUpdatedProperties],
|
// [EventListResponseEventMessagePartUpdatedProperties],
|
||||||
// [EventListResponseEventMessagePartRemovedProperties],
|
// [EventListResponseEventMessagePartRemovedProperties],
|
||||||
// [EventListResponseEventStorageWriteProperties],
|
// [EventListResponseEventStorageWriteProperties], [Permission],
|
||||||
// [EventListResponseEventFileEditedProperties], [interface{}], [Permission],
|
|
||||||
// [EventListResponseEventPermissionRepliedProperties],
|
// [EventListResponseEventPermissionRepliedProperties],
|
||||||
|
// [EventListResponseEventFileEditedProperties],
|
||||||
// [EventListResponseEventSessionUpdatedProperties],
|
// [EventListResponseEventSessionUpdatedProperties],
|
||||||
// [EventListResponseEventSessionDeletedProperties],
|
// [EventListResponseEventSessionDeletedProperties],
|
||||||
// [EventListResponseEventSessionIdleProperties],
|
// [EventListResponseEventSessionIdleProperties],
|
||||||
// [EventListResponseEventSessionErrorProperties],
|
// [EventListResponseEventSessionErrorProperties], [interface{}],
|
||||||
// [EventListResponseEventFileWatcherUpdatedProperties],
|
// [EventListResponseEventFileWatcherUpdatedProperties],
|
||||||
// [EventListResponseEventIdeInstalledProperties].
|
// [EventListResponseEventIdeInstalledProperties].
|
||||||
Properties interface{} `json:"properties,required"`
|
Properties interface{} `json:"properties,required"`
|
||||||
@@ -100,12 +100,11 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
|
|||||||
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
|
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
|
||||||
// [EventListResponseEventMessagePartUpdated],
|
// [EventListResponseEventMessagePartUpdated],
|
||||||
// [EventListResponseEventMessagePartRemoved],
|
// [EventListResponseEventMessagePartRemoved],
|
||||||
// [EventListResponseEventStorageWrite], [EventListResponseEventFileEdited],
|
// [EventListResponseEventStorageWrite], [EventListResponseEventPermissionUpdated],
|
||||||
// [EventListResponseEventServerConnected],
|
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
|
||||||
// [EventListResponseEventPermissionUpdated],
|
|
||||||
// [EventListResponseEventPermissionReplied],
|
|
||||||
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
|
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
|
||||||
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
|
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
|
||||||
|
// [EventListResponseEventServerConnected],
|
||||||
// [EventListResponseEventFileWatcherUpdated],
|
// [EventListResponseEventFileWatcherUpdated],
|
||||||
// [EventListResponseEventIdeInstalled].
|
// [EventListResponseEventIdeInstalled].
|
||||||
func (r EventListResponse) AsUnion() EventListResponseUnion {
|
func (r EventListResponse) AsUnion() EventListResponseUnion {
|
||||||
@@ -117,12 +116,11 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
|
|||||||
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
|
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
|
||||||
// [EventListResponseEventMessagePartUpdated],
|
// [EventListResponseEventMessagePartUpdated],
|
||||||
// [EventListResponseEventMessagePartRemoved],
|
// [EventListResponseEventMessagePartRemoved],
|
||||||
// [EventListResponseEventStorageWrite], [EventListResponseEventFileEdited],
|
// [EventListResponseEventStorageWrite], [EventListResponseEventPermissionUpdated],
|
||||||
// [EventListResponseEventServerConnected],
|
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
|
||||||
// [EventListResponseEventPermissionUpdated],
|
|
||||||
// [EventListResponseEventPermissionReplied],
|
|
||||||
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
|
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
|
||||||
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
|
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
|
||||||
|
// [EventListResponseEventServerConnected],
|
||||||
// [EventListResponseEventFileWatcherUpdated] or
|
// [EventListResponseEventFileWatcherUpdated] or
|
||||||
// [EventListResponseEventIdeInstalled].
|
// [EventListResponseEventIdeInstalled].
|
||||||
type EventListResponseUnion interface {
|
type EventListResponseUnion interface {
|
||||||
@@ -168,16 +166,6 @@ func init() {
|
|||||||
Type: reflect.TypeOf(EventListResponseEventStorageWrite{}),
|
Type: reflect.TypeOf(EventListResponseEventStorageWrite{}),
|
||||||
DiscriminatorValue: "storage.write",
|
DiscriminatorValue: "storage.write",
|
||||||
},
|
},
|
||||||
apijson.UnionVariant{
|
|
||||||
TypeFilter: gjson.JSON,
|
|
||||||
Type: reflect.TypeOf(EventListResponseEventFileEdited{}),
|
|
||||||
DiscriminatorValue: "file.edited",
|
|
||||||
},
|
|
||||||
apijson.UnionVariant{
|
|
||||||
TypeFilter: gjson.JSON,
|
|
||||||
Type: reflect.TypeOf(EventListResponseEventServerConnected{}),
|
|
||||||
DiscriminatorValue: "server.connected",
|
|
||||||
},
|
|
||||||
apijson.UnionVariant{
|
apijson.UnionVariant{
|
||||||
TypeFilter: gjson.JSON,
|
TypeFilter: gjson.JSON,
|
||||||
Type: reflect.TypeOf(EventListResponseEventPermissionUpdated{}),
|
Type: reflect.TypeOf(EventListResponseEventPermissionUpdated{}),
|
||||||
@@ -188,6 +176,11 @@ func init() {
|
|||||||
Type: reflect.TypeOf(EventListResponseEventPermissionReplied{}),
|
Type: reflect.TypeOf(EventListResponseEventPermissionReplied{}),
|
||||||
DiscriminatorValue: "permission.replied",
|
DiscriminatorValue: "permission.replied",
|
||||||
},
|
},
|
||||||
|
apijson.UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(EventListResponseEventFileEdited{}),
|
||||||
|
DiscriminatorValue: "file.edited",
|
||||||
|
},
|
||||||
apijson.UnionVariant{
|
apijson.UnionVariant{
|
||||||
TypeFilter: gjson.JSON,
|
TypeFilter: gjson.JSON,
|
||||||
Type: reflect.TypeOf(EventListResponseEventSessionUpdated{}),
|
Type: reflect.TypeOf(EventListResponseEventSessionUpdated{}),
|
||||||
@@ -208,6 +201,11 @@ func init() {
|
|||||||
Type: reflect.TypeOf(EventListResponseEventSessionError{}),
|
Type: reflect.TypeOf(EventListResponseEventSessionError{}),
|
||||||
DiscriminatorValue: "session.error",
|
DiscriminatorValue: "session.error",
|
||||||
},
|
},
|
||||||
|
apijson.UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(EventListResponseEventServerConnected{}),
|
||||||
|
DiscriminatorValue: "server.connected",
|
||||||
|
},
|
||||||
apijson.UnionVariant{
|
apijson.UnionVariant{
|
||||||
TypeFilter: gjson.JSON,
|
TypeFilter: gjson.JSON,
|
||||||
Type: reflect.TypeOf(EventListResponseEventFileWatcherUpdated{}),
|
Type: reflect.TypeOf(EventListResponseEventFileWatcherUpdated{}),
|
||||||
@@ -651,105 +649,6 @@ func (r EventListResponseEventStorageWriteType) IsKnown() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventListResponseEventFileEdited struct {
|
|
||||||
Properties EventListResponseEventFileEditedProperties `json:"properties,required"`
|
|
||||||
Type EventListResponseEventFileEditedType `json:"type,required"`
|
|
||||||
JSON eventListResponseEventFileEditedJSON `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventListResponseEventFileEditedJSON contains the JSON metadata for the struct
|
|
||||||
// [EventListResponseEventFileEdited]
|
|
||||||
type eventListResponseEventFileEditedJSON struct {
|
|
||||||
Properties apijson.Field
|
|
||||||
Type apijson.Field
|
|
||||||
raw string
|
|
||||||
ExtraFields map[string]apijson.Field
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *EventListResponseEventFileEdited) UnmarshalJSON(data []byte) (err error) {
|
|
||||||
return apijson.UnmarshalRoot(data, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r eventListResponseEventFileEditedJSON) RawJSON() string {
|
|
||||||
return r.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r EventListResponseEventFileEdited) implementsEventListResponse() {}
|
|
||||||
|
|
||||||
type EventListResponseEventFileEditedProperties struct {
|
|
||||||
File string `json:"file,required"`
|
|
||||||
JSON eventListResponseEventFileEditedPropertiesJSON `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventListResponseEventFileEditedPropertiesJSON contains the JSON metadata for
|
|
||||||
// the struct [EventListResponseEventFileEditedProperties]
|
|
||||||
type eventListResponseEventFileEditedPropertiesJSON struct {
|
|
||||||
File apijson.Field
|
|
||||||
raw string
|
|
||||||
ExtraFields map[string]apijson.Field
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *EventListResponseEventFileEditedProperties) UnmarshalJSON(data []byte) (err error) {
|
|
||||||
return apijson.UnmarshalRoot(data, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r eventListResponseEventFileEditedPropertiesJSON) RawJSON() string {
|
|
||||||
return r.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventListResponseEventFileEditedType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
EventListResponseEventFileEditedTypeFileEdited EventListResponseEventFileEditedType = "file.edited"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r EventListResponseEventFileEditedType) IsKnown() bool {
|
|
||||||
switch r {
|
|
||||||
case EventListResponseEventFileEditedTypeFileEdited:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventListResponseEventServerConnected struct {
|
|
||||||
Properties interface{} `json:"properties,required"`
|
|
||||||
Type EventListResponseEventServerConnectedType `json:"type,required"`
|
|
||||||
JSON eventListResponseEventServerConnectedJSON `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventListResponseEventServerConnectedJSON contains the JSON metadata for the
|
|
||||||
// struct [EventListResponseEventServerConnected]
|
|
||||||
type eventListResponseEventServerConnectedJSON struct {
|
|
||||||
Properties apijson.Field
|
|
||||||
Type apijson.Field
|
|
||||||
raw string
|
|
||||||
ExtraFields map[string]apijson.Field
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *EventListResponseEventServerConnected) UnmarshalJSON(data []byte) (err error) {
|
|
||||||
return apijson.UnmarshalRoot(data, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r eventListResponseEventServerConnectedJSON) RawJSON() string {
|
|
||||||
return r.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r EventListResponseEventServerConnected) implementsEventListResponse() {}
|
|
||||||
|
|
||||||
type EventListResponseEventServerConnectedType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
EventListResponseEventServerConnectedTypeServerConnected EventListResponseEventServerConnectedType = "server.connected"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r EventListResponseEventServerConnectedType) IsKnown() bool {
|
|
||||||
switch r {
|
|
||||||
case EventListResponseEventServerConnectedTypeServerConnected:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventListResponseEventPermissionUpdated struct {
|
type EventListResponseEventPermissionUpdated struct {
|
||||||
Properties Permission `json:"properties,required"`
|
Properties Permission `json:"properties,required"`
|
||||||
Type EventListResponseEventPermissionUpdatedType `json:"type,required"`
|
Type EventListResponseEventPermissionUpdatedType `json:"type,required"`
|
||||||
@@ -853,6 +752,66 @@ func (r EventListResponseEventPermissionRepliedType) IsKnown() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventListResponseEventFileEdited struct {
|
||||||
|
Properties EventListResponseEventFileEditedProperties `json:"properties,required"`
|
||||||
|
Type EventListResponseEventFileEditedType `json:"type,required"`
|
||||||
|
JSON eventListResponseEventFileEditedJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventListResponseEventFileEditedJSON contains the JSON metadata for the struct
|
||||||
|
// [EventListResponseEventFileEdited]
|
||||||
|
type eventListResponseEventFileEditedJSON struct {
|
||||||
|
Properties apijson.Field
|
||||||
|
Type apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EventListResponseEventFileEdited) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r eventListResponseEventFileEditedJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r EventListResponseEventFileEdited) implementsEventListResponse() {}
|
||||||
|
|
||||||
|
type EventListResponseEventFileEditedProperties struct {
|
||||||
|
File string `json:"file,required"`
|
||||||
|
JSON eventListResponseEventFileEditedPropertiesJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventListResponseEventFileEditedPropertiesJSON contains the JSON metadata for
|
||||||
|
// the struct [EventListResponseEventFileEditedProperties]
|
||||||
|
type eventListResponseEventFileEditedPropertiesJSON struct {
|
||||||
|
File apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EventListResponseEventFileEditedProperties) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r eventListResponseEventFileEditedPropertiesJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventListResponseEventFileEditedType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventListResponseEventFileEditedTypeFileEdited EventListResponseEventFileEditedType = "file.edited"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r EventListResponseEventFileEditedType) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case EventListResponseEventFileEditedTypeFileEdited:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type EventListResponseEventSessionUpdated struct {
|
type EventListResponseEventSessionUpdated struct {
|
||||||
Properties EventListResponseEventSessionUpdatedProperties `json:"properties,required"`
|
Properties EventListResponseEventSessionUpdatedProperties `json:"properties,required"`
|
||||||
Type EventListResponseEventSessionUpdatedType `json:"type,required"`
|
Type EventListResponseEventSessionUpdatedType `json:"type,required"`
|
||||||
@@ -1229,6 +1188,45 @@ func (r EventListResponseEventSessionErrorType) IsKnown() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventListResponseEventServerConnected struct {
|
||||||
|
Properties interface{} `json:"properties,required"`
|
||||||
|
Type EventListResponseEventServerConnectedType `json:"type,required"`
|
||||||
|
JSON eventListResponseEventServerConnectedJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventListResponseEventServerConnectedJSON contains the JSON metadata for the
|
||||||
|
// struct [EventListResponseEventServerConnected]
|
||||||
|
type eventListResponseEventServerConnectedJSON struct {
|
||||||
|
Properties apijson.Field
|
||||||
|
Type apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EventListResponseEventServerConnected) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r eventListResponseEventServerConnectedJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r EventListResponseEventServerConnected) implementsEventListResponse() {}
|
||||||
|
|
||||||
|
type EventListResponseEventServerConnectedType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventListResponseEventServerConnectedTypeServerConnected EventListResponseEventServerConnectedType = "server.connected"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r EventListResponseEventServerConnectedType) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case EventListResponseEventServerConnectedTypeServerConnected:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type EventListResponseEventFileWatcherUpdated struct {
|
type EventListResponseEventFileWatcherUpdated struct {
|
||||||
Properties EventListResponseEventFileWatcherUpdatedProperties `json:"properties,required"`
|
Properties EventListResponseEventFileWatcherUpdatedProperties `json:"properties,required"`
|
||||||
Type EventListResponseEventFileWatcherUpdatedType `json:"type,required"`
|
Type EventListResponseEventFileWatcherUpdatedType `json:"type,required"`
|
||||||
@@ -1376,21 +1374,21 @@ const (
|
|||||||
EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated"
|
EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated"
|
||||||
EventListResponseTypeMessagePartRemoved EventListResponseType = "message.part.removed"
|
EventListResponseTypeMessagePartRemoved EventListResponseType = "message.part.removed"
|
||||||
EventListResponseTypeStorageWrite EventListResponseType = "storage.write"
|
EventListResponseTypeStorageWrite EventListResponseType = "storage.write"
|
||||||
EventListResponseTypeFileEdited EventListResponseType = "file.edited"
|
|
||||||
EventListResponseTypeServerConnected EventListResponseType = "server.connected"
|
|
||||||
EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated"
|
EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated"
|
||||||
EventListResponseTypePermissionReplied EventListResponseType = "permission.replied"
|
EventListResponseTypePermissionReplied EventListResponseType = "permission.replied"
|
||||||
|
EventListResponseTypeFileEdited EventListResponseType = "file.edited"
|
||||||
EventListResponseTypeSessionUpdated EventListResponseType = "session.updated"
|
EventListResponseTypeSessionUpdated EventListResponseType = "session.updated"
|
||||||
EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
|
EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
|
||||||
EventListResponseTypeSessionIdle EventListResponseType = "session.idle"
|
EventListResponseTypeSessionIdle EventListResponseType = "session.idle"
|
||||||
EventListResponseTypeSessionError EventListResponseType = "session.error"
|
EventListResponseTypeSessionError EventListResponseType = "session.error"
|
||||||
|
EventListResponseTypeServerConnected EventListResponseType = "server.connected"
|
||||||
EventListResponseTypeFileWatcherUpdated EventListResponseType = "file.watcher.updated"
|
EventListResponseTypeFileWatcherUpdated EventListResponseType = "file.watcher.updated"
|
||||||
EventListResponseTypeIdeInstalled EventListResponseType = "ide.installed"
|
EventListResponseTypeIdeInstalled EventListResponseType = "ide.installed"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r EventListResponseType) IsKnown() bool {
|
func (r EventListResponseType) IsKnown() bool {
|
||||||
switch r {
|
switch r {
|
||||||
case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeStorageWrite, EventListResponseTypeFileEdited, EventListResponseTypeServerConnected, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeIdeInstalled:
|
case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeStorageWrite, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeIdeInstalled:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
+141
-65
@@ -39,10 +39,22 @@ func NewSessionService(opts ...option.RequestOption) (r *SessionService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new session
|
// Create a new session
|
||||||
func (r *SessionService) New(ctx context.Context, opts ...option.RequestOption) (res *Session, err error) {
|
func (r *SessionService) New(ctx context.Context, body SessionNewParams, opts ...option.RequestOption) (res *Session, err error) {
|
||||||
opts = append(r.Options[:], opts...)
|
opts = append(r.Options[:], opts...)
|
||||||
path := "session"
|
path := "session"
|
||||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session properties
|
||||||
|
func (r *SessionService) Update(ctx context.Context, id string, body SessionUpdateParams, opts ...option.RequestOption) (res *Session, err error) {
|
||||||
|
opts = append(r.Options[:], opts...)
|
||||||
|
if id == "" {
|
||||||
|
err = errors.New("missing required id parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("session/%s", id)
|
||||||
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, body, &res, opts...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +102,30 @@ func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatPa
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a session's children
|
||||||
|
func (r *SessionService) Children(ctx context.Context, id string, opts ...option.RequestOption) (res *[]Session, err error) {
|
||||||
|
opts = append(r.Options[:], opts...)
|
||||||
|
if id == "" {
|
||||||
|
err = errors.New("missing required id parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("session/%s/children", id)
|
||||||
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session
|
||||||
|
func (r *SessionService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) {
|
||||||
|
opts = append(r.Options[:], opts...)
|
||||||
|
if id == "" {
|
||||||
|
err = errors.New("missing required id parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("session/%s", id)
|
||||||
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Analyze the app and create an AGENTS.md file
|
// Analyze the app and create an AGENTS.md file
|
||||||
func (r *SessionService) Init(ctx context.Context, id string, body SessionInitParams, opts ...option.RequestOption) (res *bool, err error) {
|
func (r *SessionService) Init(ctx context.Context, id string, body SessionInitParams, opts ...option.RequestOption) (res *bool, err error) {
|
||||||
opts = append(r.Options[:], opts...)
|
opts = append(r.Options[:], opts...)
|
||||||
@@ -154,6 +190,18 @@ func (r *SessionService) Share(ctx context.Context, id string, opts ...option.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run a shell command
|
||||||
|
func (r *SessionService) Shell(ctx context.Context, id string, body SessionShellParams, opts ...option.RequestOption) (res *AssistantMessage, err error) {
|
||||||
|
opts = append(r.Options[:], opts...)
|
||||||
|
if id == "" {
|
||||||
|
err = errors.New("missing required id parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("session/%s/shell", id)
|
||||||
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Summarize the session
|
// Summarize the session
|
||||||
func (r *SessionService) Summarize(ctx context.Context, id string, body SessionSummarizeParams, opts ...option.RequestOption) (res *bool, err error) {
|
func (r *SessionService) Summarize(ctx context.Context, id string, body SessionSummarizeParams, opts ...option.RequestOption) (res *bool, err error) {
|
||||||
opts = append(r.Options[:], opts...)
|
opts = append(r.Options[:], opts...)
|
||||||
@@ -964,11 +1012,11 @@ type Part struct {
|
|||||||
// This field can have the runtime type of [[]string].
|
// This field can have the runtime type of [[]string].
|
||||||
Files interface{} `json:"files"`
|
Files interface{} `json:"files"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
Mime string `json:"mime"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
// This field can have the runtime type of [map[string]interface{}].
|
// This field can have the runtime type of [map[string]interface{}].
|
||||||
ProviderMetadata interface{} `json:"providerMetadata"`
|
Metadata interface{} `json:"metadata"`
|
||||||
Snapshot string `json:"snapshot"`
|
Mime string `json:"mime"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Snapshot string `json:"snapshot"`
|
||||||
// This field can have the runtime type of [FilePartSource], [AgentPartSource].
|
// This field can have the runtime type of [FilePartSource], [AgentPartSource].
|
||||||
Source interface{} `json:"source"`
|
Source interface{} `json:"source"`
|
||||||
// This field can have the runtime type of [ToolPartState].
|
// This field can have the runtime type of [ToolPartState].
|
||||||
@@ -987,29 +1035,29 @@ type Part struct {
|
|||||||
|
|
||||||
// partJSON contains the JSON metadata for the struct [Part]
|
// partJSON contains the JSON metadata for the struct [Part]
|
||||||
type partJSON struct {
|
type partJSON struct {
|
||||||
ID apijson.Field
|
ID apijson.Field
|
||||||
MessageID apijson.Field
|
MessageID apijson.Field
|
||||||
SessionID apijson.Field
|
SessionID apijson.Field
|
||||||
Type apijson.Field
|
Type apijson.Field
|
||||||
CallID apijson.Field
|
CallID apijson.Field
|
||||||
Cost apijson.Field
|
Cost apijson.Field
|
||||||
Filename apijson.Field
|
Filename apijson.Field
|
||||||
Files apijson.Field
|
Files apijson.Field
|
||||||
Hash apijson.Field
|
Hash apijson.Field
|
||||||
Mime apijson.Field
|
Metadata apijson.Field
|
||||||
Name apijson.Field
|
Mime apijson.Field
|
||||||
ProviderMetadata apijson.Field
|
Name apijson.Field
|
||||||
Snapshot apijson.Field
|
Snapshot apijson.Field
|
||||||
Source apijson.Field
|
Source apijson.Field
|
||||||
State apijson.Field
|
State apijson.Field
|
||||||
Synthetic apijson.Field
|
Synthetic apijson.Field
|
||||||
Text apijson.Field
|
Text apijson.Field
|
||||||
Time apijson.Field
|
Time apijson.Field
|
||||||
Tokens apijson.Field
|
Tokens apijson.Field
|
||||||
Tool apijson.Field
|
Tool apijson.Field
|
||||||
URL apijson.Field
|
URL apijson.Field
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r partJSON) RawJSON() string {
|
func (r partJSON) RawJSON() string {
|
||||||
@@ -1163,27 +1211,27 @@ func (r PartType) IsKnown() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ReasoningPart struct {
|
type ReasoningPart struct {
|
||||||
ID string `json:"id,required"`
|
ID string `json:"id,required"`
|
||||||
MessageID string `json:"messageID,required"`
|
MessageID string `json:"messageID,required"`
|
||||||
SessionID string `json:"sessionID,required"`
|
SessionID string `json:"sessionID,required"`
|
||||||
Text string `json:"text,required"`
|
Text string `json:"text,required"`
|
||||||
Type ReasoningPartType `json:"type,required"`
|
Time ReasoningPartTime `json:"time,required"`
|
||||||
ProviderMetadata map[string]interface{} `json:"providerMetadata"`
|
Type ReasoningPartType `json:"type,required"`
|
||||||
Time ReasoningPartTime `json:"time"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
JSON reasoningPartJSON `json:"-"`
|
JSON reasoningPartJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// reasoningPartJSON contains the JSON metadata for the struct [ReasoningPart]
|
// reasoningPartJSON contains the JSON metadata for the struct [ReasoningPart]
|
||||||
type reasoningPartJSON struct {
|
type reasoningPartJSON struct {
|
||||||
ID apijson.Field
|
ID apijson.Field
|
||||||
MessageID apijson.Field
|
MessageID apijson.Field
|
||||||
SessionID apijson.Field
|
SessionID apijson.Field
|
||||||
Text apijson.Field
|
Text apijson.Field
|
||||||
Type apijson.Field
|
Time apijson.Field
|
||||||
ProviderMetadata apijson.Field
|
Type apijson.Field
|
||||||
Time apijson.Field
|
Metadata apijson.Field
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ReasoningPart) UnmarshalJSON(data []byte) (err error) {
|
func (r *ReasoningPart) UnmarshalJSON(data []byte) (err error) {
|
||||||
@@ -1196,20 +1244,6 @@ func (r reasoningPartJSON) RawJSON() string {
|
|||||||
|
|
||||||
func (r ReasoningPart) implementsPart() {}
|
func (r ReasoningPart) implementsPart() {}
|
||||||
|
|
||||||
type ReasoningPartType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ReasoningPartTypeReasoning ReasoningPartType = "reasoning"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r ReasoningPartType) IsKnown() bool {
|
|
||||||
switch r {
|
|
||||||
case ReasoningPartTypeReasoning:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReasoningPartTime struct {
|
type ReasoningPartTime struct {
|
||||||
Start float64 `json:"start,required"`
|
Start float64 `json:"start,required"`
|
||||||
End float64 `json:"end"`
|
End float64 `json:"end"`
|
||||||
@@ -1233,6 +1267,20 @@ func (r reasoningPartTimeJSON) RawJSON() string {
|
|||||||
return r.raw
|
return r.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReasoningPartType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReasoningPartTypeReasoning ReasoningPartType = "reasoning"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r ReasoningPartType) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case ReasoningPartTypeReasoning:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
ID string `json:"id,required"`
|
ID string `json:"id,required"`
|
||||||
Time SessionTime `json:"time,required"`
|
Time SessionTime `json:"time,required"`
|
||||||
@@ -1999,11 +2047,12 @@ func (r toolStateCompletedTimeJSON) RawJSON() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ToolStateError struct {
|
type ToolStateError struct {
|
||||||
Error string `json:"error,required"`
|
Error string `json:"error,required"`
|
||||||
Input map[string]interface{} `json:"input,required"`
|
Input map[string]interface{} `json:"input,required"`
|
||||||
Status ToolStateErrorStatus `json:"status,required"`
|
Status ToolStateErrorStatus `json:"status,required"`
|
||||||
Time ToolStateErrorTime `json:"time,required"`
|
Time ToolStateErrorTime `json:"time,required"`
|
||||||
JSON toolStateErrorJSON `json:"-"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
|
JSON toolStateErrorJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolStateErrorJSON contains the JSON metadata for the struct [ToolStateError]
|
// toolStateErrorJSON contains the JSON metadata for the struct [ToolStateError]
|
||||||
@@ -2012,6 +2061,7 @@ type toolStateErrorJSON struct {
|
|||||||
Input apijson.Field
|
Input apijson.Field
|
||||||
Status apijson.Field
|
Status apijson.Field
|
||||||
Time apijson.Field
|
Time apijson.Field
|
||||||
|
Metadata apijson.Field
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
@@ -2274,6 +2324,23 @@ func (r sessionMessagesResponseJSON) RawJSON() string {
|
|||||||
return r.raw
|
return r.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionNewParams struct {
|
||||||
|
ParentID param.Field[string] `json:"parentID"`
|
||||||
|
Title param.Field[string] `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r SessionNewParams) MarshalJSON() (data []byte, err error) {
|
||||||
|
return apijson.MarshalRoot(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionUpdateParams struct {
|
||||||
|
Title param.Field[string] `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r SessionUpdateParams) MarshalJSON() (data []byte, err error) {
|
||||||
|
return apijson.MarshalRoot(r)
|
||||||
|
}
|
||||||
|
|
||||||
type SessionChatParams struct {
|
type SessionChatParams struct {
|
||||||
ModelID param.Field[string] `json:"modelID,required"`
|
ModelID param.Field[string] `json:"modelID,required"`
|
||||||
Parts param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"`
|
Parts param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"`
|
||||||
@@ -2348,6 +2415,15 @@ func (r SessionRevertParams) MarshalJSON() (data []byte, err error) {
|
|||||||
return apijson.MarshalRoot(r)
|
return apijson.MarshalRoot(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionShellParams struct {
|
||||||
|
Agent param.Field[string] `json:"agent,required"`
|
||||||
|
Command param.Field[string] `json:"command,required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r SessionShellParams) MarshalJSON() (data []byte, err error) {
|
||||||
|
return apijson.MarshalRoot(r)
|
||||||
|
}
|
||||||
|
|
||||||
type SessionSummarizeParams struct {
|
type SessionSummarizeParams struct {
|
||||||
ModelID param.Field[string] `json:"modelID,required"`
|
ModelID param.Field[string] `json:"modelID,required"`
|
||||||
ProviderID param.Field[string] `json:"providerID,required"`
|
ProviderID param.Field[string] `json:"providerID,required"`
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/sst/opencode-sdk-go/option"
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSessionNew(t *testing.T) {
|
func TestSessionNewWithOptionalParams(t *testing.T) {
|
||||||
t.Skip("skipped: tests are disabled for the time being")
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
baseURL := "http://localhost:4010"
|
baseURL := "http://localhost:4010"
|
||||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
@@ -25,7 +25,38 @@ func TestSessionNew(t *testing.T) {
|
|||||||
client := opencode.NewClient(
|
client := opencode.NewClient(
|
||||||
option.WithBaseURL(baseURL),
|
option.WithBaseURL(baseURL),
|
||||||
)
|
)
|
||||||
_, err := client.Session.New(context.TODO())
|
_, err := client.Session.New(context.TODO(), opencode.SessionNewParams{
|
||||||
|
ParentID: opencode.F("parentID"),
|
||||||
|
Title: opencode.F("title"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionUpdateWithOptionalParams(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.Session.Update(
|
||||||
|
context.TODO(),
|
||||||
|
"id",
|
||||||
|
opencode.SessionUpdateParams{
|
||||||
|
Title: opencode.F("title"),
|
||||||
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var apierr *opencode.Error
|
var apierr *opencode.Error
|
||||||
if errors.As(err, &apierr) {
|
if errors.As(err, &apierr) {
|
||||||
@@ -146,6 +177,50 @@ func TestSessionChatWithOptionalParams(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSessionChildren(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.Session.Children(context.TODO(), "id")
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionGet(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.Session.Get(context.TODO(), "id")
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSessionInit(t *testing.T) {
|
func TestSessionInit(t *testing.T) {
|
||||||
t.Skip("skipped: tests are disabled for the time being")
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
baseURL := "http://localhost:4010"
|
baseURL := "http://localhost:4010"
|
||||||
@@ -275,6 +350,35 @@ func TestSessionShare(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSessionShell(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.Session.Shell(
|
||||||
|
context.TODO(),
|
||||||
|
"id",
|
||||||
|
opencode.SessionShellParams{
|
||||||
|
Agent: opencode.F("agent"),
|
||||||
|
Command: opencode.F("command"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSessionSummarize(t *testing.T) {
|
func TestSessionSummarize(t *testing.T) {
|
||||||
t.Skip("skipped: tests are disabled for the time being")
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
baseURL := "http://localhost:4010"
|
baseURL := "http://localhost:4010"
|
||||||
|
|||||||
+36
-1
@@ -47,7 +47,7 @@ func (r *TuiService) ClearPrompt(ctx context.Context, opts ...option.RequestOpti
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute a TUI command (e.g. switch_agent)
|
// Execute a TUI command (e.g. agent_cycle)
|
||||||
func (r *TuiService) ExecuteCommand(ctx context.Context, body TuiExecuteCommandParams, opts ...option.RequestOption) (res *bool, err error) {
|
func (r *TuiService) ExecuteCommand(ctx context.Context, body TuiExecuteCommandParams, opts ...option.RequestOption) (res *bool, err error) {
|
||||||
opts = append(r.Options[:], opts...)
|
opts = append(r.Options[:], opts...)
|
||||||
path := "tui/execute-command"
|
path := "tui/execute-command"
|
||||||
@@ -87,6 +87,14 @@ func (r *TuiService) OpenThemes(ctx context.Context, opts ...option.RequestOptio
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show a toast notification in the TUI
|
||||||
|
func (r *TuiService) ShowToast(ctx context.Context, body TuiShowToastParams, opts ...option.RequestOption) (res *bool, err error) {
|
||||||
|
opts = append(r.Options[:], opts...)
|
||||||
|
path := "tui/show-toast"
|
||||||
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Submit the prompt
|
// Submit the prompt
|
||||||
func (r *TuiService) SubmitPrompt(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
|
func (r *TuiService) SubmitPrompt(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
|
||||||
opts = append(r.Options[:], opts...)
|
opts = append(r.Options[:], opts...)
|
||||||
@@ -110,3 +118,30 @@ type TuiExecuteCommandParams struct {
|
|||||||
func (r TuiExecuteCommandParams) MarshalJSON() (data []byte, err error) {
|
func (r TuiExecuteCommandParams) MarshalJSON() (data []byte, err error) {
|
||||||
return apijson.MarshalRoot(r)
|
return apijson.MarshalRoot(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TuiShowToastParams struct {
|
||||||
|
Message param.Field[string] `json:"message,required"`
|
||||||
|
Variant param.Field[TuiShowToastParamsVariant] `json:"variant,required"`
|
||||||
|
Title param.Field[string] `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r TuiShowToastParams) MarshalJSON() (data []byte, err error) {
|
||||||
|
return apijson.MarshalRoot(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TuiShowToastParamsVariant string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TuiShowToastParamsVariantInfo TuiShowToastParamsVariant = "info"
|
||||||
|
TuiShowToastParamsVariantSuccess TuiShowToastParamsVariant = "success"
|
||||||
|
TuiShowToastParamsVariantWarning TuiShowToastParamsVariant = "warning"
|
||||||
|
TuiShowToastParamsVariantError TuiShowToastParamsVariant = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r TuiShowToastParamsVariant) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case TuiShowToastParamsVariantInfo, TuiShowToastParamsVariantSuccess, TuiShowToastParamsVariantWarning, TuiShowToastParamsVariantError:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -171,6 +171,32 @@ func TestTuiOpenThemes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTuiShowToastWithOptionalParams(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.Tui.ShowToast(context.TODO(), opencode.TuiShowToastParams{
|
||||||
|
Message: opencode.F("message"),
|
||||||
|
Variant: opencode.F(opencode.TuiShowToastParamsVariantInfo),
|
||||||
|
Title: opencode.F("title"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTuiSubmitPrompt(t *testing.T) {
|
func TestTuiSubmitPrompt(t *testing.T) {
|
||||||
t.Skip("skipped: tests are disabled for the time being")
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
baseURL := "http://localhost:4010"
|
baseURL := "http://localhost:4010"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "0.4.26",
|
"version": "0.5.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import type {
|
|||||||
SessionDeleteResponses,
|
SessionDeleteResponses,
|
||||||
SessionGetData,
|
SessionGetData,
|
||||||
SessionGetResponses,
|
SessionGetResponses,
|
||||||
|
SessionUpdateData,
|
||||||
|
SessionUpdateResponses,
|
||||||
|
SessionChildrenData,
|
||||||
|
SessionChildrenResponses,
|
||||||
SessionInitData,
|
SessionInitData,
|
||||||
SessionInitResponses,
|
SessionInitResponses,
|
||||||
SessionAbortData,
|
SessionAbortData,
|
||||||
@@ -35,6 +39,8 @@ import type {
|
|||||||
SessionChatResponses,
|
SessionChatResponses,
|
||||||
SessionMessageData,
|
SessionMessageData,
|
||||||
SessionMessageResponses,
|
SessionMessageResponses,
|
||||||
|
SessionShellData,
|
||||||
|
SessionShellResponses,
|
||||||
SessionRevertData,
|
SessionRevertData,
|
||||||
SessionRevertResponses,
|
SessionRevertResponses,
|
||||||
SessionUnrevertData,
|
SessionUnrevertData,
|
||||||
@@ -73,6 +79,11 @@ import type {
|
|||||||
TuiClearPromptResponses,
|
TuiClearPromptResponses,
|
||||||
TuiExecuteCommandData,
|
TuiExecuteCommandData,
|
||||||
TuiExecuteCommandResponses,
|
TuiExecuteCommandResponses,
|
||||||
|
TuiShowToastData,
|
||||||
|
TuiShowToastResponses,
|
||||||
|
AuthSetData,
|
||||||
|
AuthSetResponses,
|
||||||
|
AuthSetErrors,
|
||||||
} from "./types.gen.js"
|
} from "./types.gen.js"
|
||||||
import { client as _heyApiClient } from "./client.gen.js"
|
import { client as _heyApiClient } from "./client.gen.js"
|
||||||
|
|
||||||
@@ -201,6 +212,10 @@ class Session extends _HeyApiClient {
|
|||||||
return (options?.client ?? this._client).post<SessionCreateResponses, SessionCreateErrors, ThrowOnError>({
|
return (options?.client ?? this._client).post<SessionCreateResponses, SessionCreateErrors, ThrowOnError>({
|
||||||
url: "/session",
|
url: "/session",
|
||||||
...options,
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options?.headers,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +239,30 @@ class Session extends _HeyApiClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update session properties
|
||||||
|
*/
|
||||||
|
public update<ThrowOnError extends boolean = false>(options: Options<SessionUpdateData, ThrowOnError>) {
|
||||||
|
return (options.client ?? this._client).patch<SessionUpdateResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/session/{id}",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a session's children
|
||||||
|
*/
|
||||||
|
public children<ThrowOnError extends boolean = false>(options: Options<SessionChildrenData, ThrowOnError>) {
|
||||||
|
return (options.client ?? this._client).get<SessionChildrenResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/session/{id}/children",
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyze the app and create an AGENTS.md file
|
* Analyze the app and create an AGENTS.md file
|
||||||
*/
|
*/
|
||||||
@@ -316,6 +355,20 @@ class Session extends _HeyApiClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a shell command
|
||||||
|
*/
|
||||||
|
public shell<ThrowOnError extends boolean = false>(options: Options<SessionShellData, ThrowOnError>) {
|
||||||
|
return (options.client ?? this._client).post<SessionShellResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/session/{id}/shell",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revert a message
|
* Revert a message
|
||||||
*/
|
*/
|
||||||
@@ -471,7 +524,7 @@ class Tui extends _HeyApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a TUI command (e.g. switch_agent)
|
* Execute a TUI command (e.g. agent_cycle)
|
||||||
*/
|
*/
|
||||||
public executeCommand<ThrowOnError extends boolean = false>(options?: Options<TuiExecuteCommandData, ThrowOnError>) {
|
public executeCommand<ThrowOnError extends boolean = false>(options?: Options<TuiExecuteCommandData, ThrowOnError>) {
|
||||||
return (options?.client ?? this._client).post<TuiExecuteCommandResponses, unknown, ThrowOnError>({
|
return (options?.client ?? this._client).post<TuiExecuteCommandResponses, unknown, ThrowOnError>({
|
||||||
@@ -483,6 +536,36 @@ class Tui extends _HeyApiClient {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a toast notification in the TUI
|
||||||
|
*/
|
||||||
|
public showToast<ThrowOnError extends boolean = false>(options?: Options<TuiShowToastData, ThrowOnError>) {
|
||||||
|
return (options?.client ?? this._client).post<TuiShowToastResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/tui/show-toast",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options?.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Auth extends _HeyApiClient {
|
||||||
|
/**
|
||||||
|
* Set authentication credentials
|
||||||
|
*/
|
||||||
|
public set<ThrowOnError extends boolean = false>(options: Options<AuthSetData, ThrowOnError>) {
|
||||||
|
return (options.client ?? this._client).put<AuthSetResponses, AuthSetErrors, ThrowOnError>({
|
||||||
|
url: "/auth/{id}",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OpencodeClient extends _HeyApiClient {
|
export class OpencodeClient extends _HeyApiClient {
|
||||||
@@ -512,4 +595,5 @@ export class OpencodeClient extends _HeyApiClient {
|
|||||||
find = new Find({ client: this._client })
|
find = new Find({ client: this._client })
|
||||||
file = new File({ client: this._client })
|
file = new File({ client: this._client })
|
||||||
tui = new Tui({ client: this._client })
|
tui = new Tui({ client: this._client })
|
||||||
|
auth = new Auth({ client: this._client })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,15 @@ export type Event =
|
|||||||
| ({
|
| ({
|
||||||
type: "storage.write"
|
type: "storage.write"
|
||||||
} & EventStorageWrite)
|
} & EventStorageWrite)
|
||||||
| ({
|
|
||||||
type: "file.edited"
|
|
||||||
} & EventFileEdited)
|
|
||||||
| ({
|
|
||||||
type: "server.connected"
|
|
||||||
} & EventServerConnected)
|
|
||||||
| ({
|
| ({
|
||||||
type: "permission.updated"
|
type: "permission.updated"
|
||||||
} & EventPermissionUpdated)
|
} & EventPermissionUpdated)
|
||||||
| ({
|
| ({
|
||||||
type: "permission.replied"
|
type: "permission.replied"
|
||||||
} & EventPermissionReplied)
|
} & EventPermissionReplied)
|
||||||
|
| ({
|
||||||
|
type: "file.edited"
|
||||||
|
} & EventFileEdited)
|
||||||
| ({
|
| ({
|
||||||
type: "session.updated"
|
type: "session.updated"
|
||||||
} & EventSessionUpdated)
|
} & EventSessionUpdated)
|
||||||
@@ -46,6 +43,9 @@ export type Event =
|
|||||||
| ({
|
| ({
|
||||||
type: "session.error"
|
type: "session.error"
|
||||||
} & EventSessionError)
|
} & EventSessionError)
|
||||||
|
| ({
|
||||||
|
type: "server.connected"
|
||||||
|
} & EventServerConnected)
|
||||||
| ({
|
| ({
|
||||||
type: "file.watcher.updated"
|
type: "file.watcher.updated"
|
||||||
} & EventFileWatcherUpdated)
|
} & EventFileWatcherUpdated)
|
||||||
@@ -54,14 +54,14 @@ export type Event =
|
|||||||
} & EventIdeInstalled)
|
} & EventIdeInstalled)
|
||||||
|
|
||||||
export type EventInstallationUpdated = {
|
export type EventInstallationUpdated = {
|
||||||
type: string
|
type: "installation.updated"
|
||||||
properties: {
|
properties: {
|
||||||
version: string
|
version: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventLspClientDiagnostics = {
|
export type EventLspClientDiagnostics = {
|
||||||
type: string
|
type: "lsp.client.diagnostics"
|
||||||
properties: {
|
properties: {
|
||||||
serverID: string
|
serverID: string
|
||||||
path: string
|
path: string
|
||||||
@@ -69,7 +69,7 @@ export type EventLspClientDiagnostics = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EventMessageUpdated = {
|
export type EventMessageUpdated = {
|
||||||
type: string
|
type: "message.updated"
|
||||||
properties: {
|
properties: {
|
||||||
info: Message
|
info: Message
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ export type Message =
|
|||||||
export type UserMessage = {
|
export type UserMessage = {
|
||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
role: string
|
role: "user"
|
||||||
time: {
|
time: {
|
||||||
created: number
|
created: number
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ export type UserMessage = {
|
|||||||
export type AssistantMessage = {
|
export type AssistantMessage = {
|
||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
role: string
|
role: "assistant"
|
||||||
time: {
|
time: {
|
||||||
created: number
|
created: number
|
||||||
completed?: number
|
completed?: number
|
||||||
@@ -135,7 +135,7 @@ export type AssistantMessage = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ProviderAuthError = {
|
export type ProviderAuthError = {
|
||||||
name: string
|
name: "ProviderAuthError"
|
||||||
data: {
|
data: {
|
||||||
providerID: string
|
providerID: string
|
||||||
message: string
|
message: string
|
||||||
@@ -143,28 +143,28 @@ export type ProviderAuthError = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type UnknownError = {
|
export type UnknownError = {
|
||||||
name: string
|
name: "UnknownError"
|
||||||
data: {
|
data: {
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageOutputLengthError = {
|
export type MessageOutputLengthError = {
|
||||||
name: string
|
name: "MessageOutputLengthError"
|
||||||
data: {
|
data: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageAbortedError = {
|
export type MessageAbortedError = {
|
||||||
name: string
|
name: "MessageAbortedError"
|
||||||
data: {
|
data: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventMessageRemoved = {
|
export type EventMessageRemoved = {
|
||||||
type: string
|
type: "message.removed"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
@@ -172,7 +172,7 @@ export type EventMessageRemoved = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EventMessagePartUpdated = {
|
export type EventMessagePartUpdated = {
|
||||||
type: string
|
type: "message.part.updated"
|
||||||
properties: {
|
properties: {
|
||||||
part: Part
|
part: Part
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ export type TextPart = {
|
|||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: "text"
|
||||||
text: string
|
text: string
|
||||||
synthetic?: boolean
|
synthetic?: boolean
|
||||||
time?: {
|
time?: {
|
||||||
@@ -224,7 +224,7 @@ export type ReasoningPart = {
|
|||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: "reasoning"
|
||||||
text: string
|
text: string
|
||||||
metadata?: {
|
metadata?: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
@@ -239,7 +239,7 @@ export type FilePart = {
|
|||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: "file"
|
||||||
mime: string
|
mime: string
|
||||||
filename?: string
|
filename?: string
|
||||||
url: string
|
url: string
|
||||||
@@ -256,7 +256,7 @@ export type FilePartSource =
|
|||||||
|
|
||||||
export type FileSource = {
|
export type FileSource = {
|
||||||
text: FilePartSourceText
|
text: FilePartSourceText
|
||||||
type: string
|
type: "file"
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ export type FilePartSourceText = {
|
|||||||
|
|
||||||
export type SymbolSource = {
|
export type SymbolSource = {
|
||||||
text: FilePartSourceText
|
text: FilePartSourceText
|
||||||
type: string
|
type: "symbol"
|
||||||
path: string
|
path: string
|
||||||
range: Range
|
range: Range
|
||||||
name: string
|
name: string
|
||||||
@@ -290,7 +290,7 @@ export type ToolPart = {
|
|||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: "tool"
|
||||||
callID: string
|
callID: string
|
||||||
tool: string
|
tool: string
|
||||||
state: ToolState
|
state: ToolState
|
||||||
@@ -311,11 +311,11 @@ export type ToolState =
|
|||||||
} & ToolStateError)
|
} & ToolStateError)
|
||||||
|
|
||||||
export type ToolStatePending = {
|
export type ToolStatePending = {
|
||||||
status: string
|
status: "pending"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolStateRunning = {
|
export type ToolStateRunning = {
|
||||||
status: string
|
status: "running"
|
||||||
input?: unknown
|
input?: unknown
|
||||||
title?: string
|
title?: string
|
||||||
metadata?: {
|
metadata?: {
|
||||||
@@ -327,7 +327,7 @@ export type ToolStateRunning = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ToolStateCompleted = {
|
export type ToolStateCompleted = {
|
||||||
status: string
|
status: "completed"
|
||||||
input: {
|
input: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
@@ -343,11 +343,14 @@ export type ToolStateCompleted = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ToolStateError = {
|
export type ToolStateError = {
|
||||||
status: string
|
status: "error"
|
||||||
input: {
|
input: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
error: string
|
error: string
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
time: {
|
time: {
|
||||||
start: number
|
start: number
|
||||||
end: number
|
end: number
|
||||||
@@ -358,14 +361,14 @@ export type StepStartPart = {
|
|||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: "step-start"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StepFinishPart = {
|
export type StepFinishPart = {
|
||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: "step-finish"
|
||||||
cost: number
|
cost: number
|
||||||
tokens: {
|
tokens: {
|
||||||
input: number
|
input: number
|
||||||
@@ -382,7 +385,7 @@ export type SnapshotPart = {
|
|||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: "snapshot"
|
||||||
snapshot: string
|
snapshot: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +393,7 @@ export type PatchPart = {
|
|||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: "patch"
|
||||||
hash: string
|
hash: string
|
||||||
files: Array<string>
|
files: Array<string>
|
||||||
}
|
}
|
||||||
@@ -399,7 +402,7 @@ export type AgentPart = {
|
|||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: "agent"
|
||||||
name: string
|
name: string
|
||||||
source?: {
|
source?: {
|
||||||
value: string
|
value: string
|
||||||
@@ -409,7 +412,7 @@ export type AgentPart = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EventMessagePartRemoved = {
|
export type EventMessagePartRemoved = {
|
||||||
type: string
|
type: "message.part.removed"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
@@ -418,29 +421,15 @@ export type EventMessagePartRemoved = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EventStorageWrite = {
|
export type EventStorageWrite = {
|
||||||
type: string
|
type: "storage.write"
|
||||||
properties: {
|
properties: {
|
||||||
key: string
|
key: string
|
||||||
content?: unknown
|
content?: unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventFileEdited = {
|
|
||||||
type: string
|
|
||||||
properties: {
|
|
||||||
file: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventServerConnected = {
|
|
||||||
type: string
|
|
||||||
properties: {
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventPermissionUpdated = {
|
export type EventPermissionUpdated = {
|
||||||
type: string
|
type: "permission.updated"
|
||||||
properties: Permission
|
properties: Permission
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +450,7 @@ export type Permission = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EventPermissionReplied = {
|
export type EventPermissionReplied = {
|
||||||
type: string
|
type: "permission.replied"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
permissionID: string
|
permissionID: string
|
||||||
@@ -469,8 +458,15 @@ export type EventPermissionReplied = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventFileEdited = {
|
||||||
|
type: "file.edited"
|
||||||
|
properties: {
|
||||||
|
file: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type EventSessionUpdated = {
|
export type EventSessionUpdated = {
|
||||||
type: string
|
type: "session.updated"
|
||||||
properties: {
|
properties: {
|
||||||
info: Session
|
info: Session
|
||||||
}
|
}
|
||||||
@@ -497,21 +493,21 @@ export type Session = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EventSessionDeleted = {
|
export type EventSessionDeleted = {
|
||||||
type: string
|
type: "session.deleted"
|
||||||
properties: {
|
properties: {
|
||||||
info: Session
|
info: Session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventSessionIdle = {
|
export type EventSessionIdle = {
|
||||||
type: string
|
type: "session.idle"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventSessionError = {
|
export type EventSessionError = {
|
||||||
type: string
|
type: "session.error"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
error?:
|
error?:
|
||||||
@@ -530,16 +526,23 @@ export type EventSessionError = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventServerConnected = {
|
||||||
|
type: "server.connected"
|
||||||
|
properties: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type EventFileWatcherUpdated = {
|
export type EventFileWatcherUpdated = {
|
||||||
type: string
|
type: "file.watcher.updated"
|
||||||
properties: {
|
properties: {
|
||||||
file: string
|
file: string
|
||||||
event: string
|
event: "rename" | "change"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventIdeInstalled = {
|
export type EventIdeInstalled = {
|
||||||
type: string
|
type: "ide.installed"
|
||||||
properties: {
|
properties: {
|
||||||
ide: string
|
ide: string
|
||||||
}
|
}
|
||||||
@@ -569,7 +572,19 @@ export type Config = {
|
|||||||
* Theme name to use for the interface
|
* Theme name to use for the interface
|
||||||
*/
|
*/
|
||||||
theme?: string
|
theme?: string
|
||||||
|
/**
|
||||||
|
* Custom keybind configurations
|
||||||
|
*/
|
||||||
keybinds?: KeybindsConfig
|
keybinds?: KeybindsConfig
|
||||||
|
/**
|
||||||
|
* TUI specific settings
|
||||||
|
*/
|
||||||
|
tui?: {
|
||||||
|
/**
|
||||||
|
* TUI scroll speed
|
||||||
|
*/
|
||||||
|
scroll_speed: number
|
||||||
|
}
|
||||||
plugin?: Array<string>
|
plugin?: Array<string>
|
||||||
snapshot?: boolean
|
snapshot?: boolean
|
||||||
/**
|
/**
|
||||||
@@ -683,7 +698,7 @@ export type Config = {
|
|||||||
lsp?: {
|
lsp?: {
|
||||||
[key: string]:
|
[key: string]:
|
||||||
| {
|
| {
|
||||||
disabled: boolean
|
disabled: true
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
command: Array<string>
|
command: Array<string>
|
||||||
@@ -701,15 +716,18 @@ export type Config = {
|
|||||||
* Additional instruction files or patterns to include
|
* Additional instruction files or patterns to include
|
||||||
*/
|
*/
|
||||||
instructions?: Array<string>
|
instructions?: Array<string>
|
||||||
|
/**
|
||||||
|
* @deprecated Always uses stretch layout.
|
||||||
|
*/
|
||||||
layout?: LayoutConfig
|
layout?: LayoutConfig
|
||||||
permission?: {
|
permission?: {
|
||||||
edit?: string
|
edit?: "ask" | "allow" | "deny"
|
||||||
bash?:
|
bash?:
|
||||||
| string
|
| ("ask" | "allow" | "deny")
|
||||||
| {
|
| {
|
||||||
[key: string]: string
|
[key: string]: "ask" | "allow" | "deny"
|
||||||
}
|
}
|
||||||
webfetch?: string
|
webfetch?: "ask" | "allow" | "deny"
|
||||||
}
|
}
|
||||||
experimental?: {
|
experimental?: {
|
||||||
hook?: {
|
hook?: {
|
||||||
@@ -741,25 +759,29 @@ export type KeybindsConfig = {
|
|||||||
*/
|
*/
|
||||||
app_help: string
|
app_help: string
|
||||||
/**
|
/**
|
||||||
* @deprecated use switch_agent. Next mode
|
* Exit the application
|
||||||
*/
|
*/
|
||||||
switch_mode: string
|
app_exit: string
|
||||||
/**
|
|
||||||
* @deprecated use switch_agent_reverse. Previous mode
|
|
||||||
*/
|
|
||||||
switch_mode_reverse: string
|
|
||||||
/**
|
|
||||||
* Next agent
|
|
||||||
*/
|
|
||||||
switch_agent: string
|
|
||||||
/**
|
|
||||||
* Previous agent
|
|
||||||
*/
|
|
||||||
switch_agent_reverse: string
|
|
||||||
/**
|
/**
|
||||||
* Open external editor
|
* Open external editor
|
||||||
*/
|
*/
|
||||||
editor_open: string
|
editor_open: string
|
||||||
|
/**
|
||||||
|
* List available themes
|
||||||
|
*/
|
||||||
|
theme_list: string
|
||||||
|
/**
|
||||||
|
* Create/update AGENTS.md
|
||||||
|
*/
|
||||||
|
project_init: string
|
||||||
|
/**
|
||||||
|
* Toggle tool details
|
||||||
|
*/
|
||||||
|
tool_details: string
|
||||||
|
/**
|
||||||
|
* Toggle thinking blocks
|
||||||
|
*/
|
||||||
|
thinking_blocks: string
|
||||||
/**
|
/**
|
||||||
* Export session to editor
|
* Export session to editor
|
||||||
*/
|
*/
|
||||||
@@ -789,57 +811,13 @@ export type KeybindsConfig = {
|
|||||||
*/
|
*/
|
||||||
session_compact: string
|
session_compact: string
|
||||||
/**
|
/**
|
||||||
* Toggle tool details
|
* Cycle to next child session
|
||||||
*/
|
*/
|
||||||
tool_details: string
|
session_child_cycle: string
|
||||||
/**
|
/**
|
||||||
* Toggle thinking blocks
|
* Cycle to previous child session
|
||||||
*/
|
*/
|
||||||
thinking_blocks: string
|
session_child_cycle_reverse: string
|
||||||
/**
|
|
||||||
* List available models
|
|
||||||
*/
|
|
||||||
model_list: string
|
|
||||||
/**
|
|
||||||
* List available themes
|
|
||||||
*/
|
|
||||||
theme_list: string
|
|
||||||
/**
|
|
||||||
* List files
|
|
||||||
*/
|
|
||||||
file_list: string
|
|
||||||
/**
|
|
||||||
* Close file
|
|
||||||
*/
|
|
||||||
file_close: string
|
|
||||||
/**
|
|
||||||
* Search file
|
|
||||||
*/
|
|
||||||
file_search: string
|
|
||||||
/**
|
|
||||||
* Split/unified diff
|
|
||||||
*/
|
|
||||||
file_diff_toggle: string
|
|
||||||
/**
|
|
||||||
* Create/update AGENTS.md
|
|
||||||
*/
|
|
||||||
project_init: string
|
|
||||||
/**
|
|
||||||
* Clear input field
|
|
||||||
*/
|
|
||||||
input_clear: string
|
|
||||||
/**
|
|
||||||
* Paste from clipboard
|
|
||||||
*/
|
|
||||||
input_paste: string
|
|
||||||
/**
|
|
||||||
* Submit input
|
|
||||||
*/
|
|
||||||
input_submit: string
|
|
||||||
/**
|
|
||||||
* Insert newline in input
|
|
||||||
*/
|
|
||||||
input_newline: string
|
|
||||||
/**
|
/**
|
||||||
* Scroll messages up by one page
|
* Scroll messages up by one page
|
||||||
*/
|
*/
|
||||||
@@ -856,14 +834,6 @@ export type KeybindsConfig = {
|
|||||||
* Scroll messages down by half page
|
* Scroll messages down by half page
|
||||||
*/
|
*/
|
||||||
messages_half_page_down: string
|
messages_half_page_down: string
|
||||||
/**
|
|
||||||
* Navigate to previous message
|
|
||||||
*/
|
|
||||||
messages_previous: string
|
|
||||||
/**
|
|
||||||
* Navigate to next message
|
|
||||||
*/
|
|
||||||
messages_next: string
|
|
||||||
/**
|
/**
|
||||||
* Navigate to first message
|
* Navigate to first message
|
||||||
*/
|
*/
|
||||||
@@ -872,18 +842,10 @@ export type KeybindsConfig = {
|
|||||||
* Navigate to last message
|
* Navigate to last message
|
||||||
*/
|
*/
|
||||||
messages_last: string
|
messages_last: string
|
||||||
/**
|
|
||||||
* Toggle layout
|
|
||||||
*/
|
|
||||||
messages_layout_toggle: string
|
|
||||||
/**
|
/**
|
||||||
* Copy message
|
* Copy message
|
||||||
*/
|
*/
|
||||||
messages_copy: string
|
messages_copy: string
|
||||||
/**
|
|
||||||
* @deprecated use messages_undo. Revert message
|
|
||||||
*/
|
|
||||||
messages_revert: string
|
|
||||||
/**
|
/**
|
||||||
* Undo message
|
* Undo message
|
||||||
*/
|
*/
|
||||||
@@ -893,9 +855,93 @@ export type KeybindsConfig = {
|
|||||||
*/
|
*/
|
||||||
messages_redo: string
|
messages_redo: string
|
||||||
/**
|
/**
|
||||||
* Exit the application
|
* List available models
|
||||||
*/
|
*/
|
||||||
app_exit: string
|
model_list: string
|
||||||
|
/**
|
||||||
|
* Next recent model
|
||||||
|
*/
|
||||||
|
model_cycle_recent: string
|
||||||
|
/**
|
||||||
|
* Previous recent model
|
||||||
|
*/
|
||||||
|
model_cycle_recent_reverse: string
|
||||||
|
/**
|
||||||
|
* List agents
|
||||||
|
*/
|
||||||
|
agent_list: string
|
||||||
|
/**
|
||||||
|
* Next agent
|
||||||
|
*/
|
||||||
|
agent_cycle: string
|
||||||
|
/**
|
||||||
|
* Previous agent
|
||||||
|
*/
|
||||||
|
agent_cycle_reverse: string
|
||||||
|
/**
|
||||||
|
* Clear input field
|
||||||
|
*/
|
||||||
|
input_clear: string
|
||||||
|
/**
|
||||||
|
* Paste from clipboard
|
||||||
|
*/
|
||||||
|
input_paste: string
|
||||||
|
/**
|
||||||
|
* Submit input
|
||||||
|
*/
|
||||||
|
input_submit: string
|
||||||
|
/**
|
||||||
|
* Insert newline in input
|
||||||
|
*/
|
||||||
|
input_newline: string
|
||||||
|
/**
|
||||||
|
* @deprecated use agent_cycle. Next mode
|
||||||
|
*/
|
||||||
|
switch_mode: string
|
||||||
|
/**
|
||||||
|
* @deprecated use agent_cycle_reverse. Previous mode
|
||||||
|
*/
|
||||||
|
switch_mode_reverse: string
|
||||||
|
/**
|
||||||
|
* @deprecated use agent_cycle. Next agent
|
||||||
|
*/
|
||||||
|
switch_agent: string
|
||||||
|
/**
|
||||||
|
* @deprecated use agent_cycle_reverse. Previous agent
|
||||||
|
*/
|
||||||
|
switch_agent_reverse: string
|
||||||
|
/**
|
||||||
|
* @deprecated Currently not available. List files
|
||||||
|
*/
|
||||||
|
file_list: string
|
||||||
|
/**
|
||||||
|
* @deprecated Close file
|
||||||
|
*/
|
||||||
|
file_close: string
|
||||||
|
/**
|
||||||
|
* @deprecated Search file
|
||||||
|
*/
|
||||||
|
file_search: string
|
||||||
|
/**
|
||||||
|
* @deprecated Split/unified diff
|
||||||
|
*/
|
||||||
|
file_diff_toggle: string
|
||||||
|
/**
|
||||||
|
* @deprecated Navigate to previous message
|
||||||
|
*/
|
||||||
|
messages_previous: string
|
||||||
|
/**
|
||||||
|
* @deprecated Navigate to next message
|
||||||
|
*/
|
||||||
|
messages_next: string
|
||||||
|
/**
|
||||||
|
* @deprecated Toggle layout
|
||||||
|
*/
|
||||||
|
messages_layout_toggle: string
|
||||||
|
/**
|
||||||
|
* @deprecated use messages_undo. Revert message
|
||||||
|
*/
|
||||||
|
messages_revert: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentConfig = {
|
export type AgentConfig = {
|
||||||
@@ -911,7 +957,16 @@ export type AgentConfig = {
|
|||||||
* Description of when to use the agent
|
* Description of when to use the agent
|
||||||
*/
|
*/
|
||||||
description?: string
|
description?: string
|
||||||
mode?: string
|
mode?: "subagent" | "primary" | "all"
|
||||||
|
permission?: {
|
||||||
|
edit?: "ask" | "allow" | "deny"
|
||||||
|
bash?:
|
||||||
|
| ("ask" | "allow" | "deny")
|
||||||
|
| {
|
||||||
|
[key: string]: "ask" | "allow" | "deny"
|
||||||
|
}
|
||||||
|
webfetch?: "ask" | "allow" | "deny"
|
||||||
|
}
|
||||||
[key: string]:
|
[key: string]:
|
||||||
| unknown
|
| unknown
|
||||||
| string
|
| string
|
||||||
@@ -920,7 +975,16 @@ export type AgentConfig = {
|
|||||||
[key: string]: boolean
|
[key: string]: boolean
|
||||||
}
|
}
|
||||||
| boolean
|
| boolean
|
||||||
| string
|
| ("subagent" | "primary" | "all")
|
||||||
|
| {
|
||||||
|
edit?: "ask" | "allow" | "deny"
|
||||||
|
bash?:
|
||||||
|
| ("ask" | "allow" | "deny")
|
||||||
|
| {
|
||||||
|
[key: string]: "ask" | "allow" | "deny"
|
||||||
|
}
|
||||||
|
webfetch?: "ask" | "allow" | "deny"
|
||||||
|
}
|
||||||
| undefined
|
| undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,7 +1026,7 @@ export type McpLocalConfig = {
|
|||||||
/**
|
/**
|
||||||
* Type of MCP server connection
|
* Type of MCP server connection
|
||||||
*/
|
*/
|
||||||
type: string
|
type: "local"
|
||||||
/**
|
/**
|
||||||
* Command and arguments to run the MCP server
|
* Command and arguments to run the MCP server
|
||||||
*/
|
*/
|
||||||
@@ -983,7 +1047,7 @@ export type McpRemoteConfig = {
|
|||||||
/**
|
/**
|
||||||
* Type of MCP server connection
|
* Type of MCP server connection
|
||||||
*/
|
*/
|
||||||
type: string
|
type: "remote"
|
||||||
/**
|
/**
|
||||||
* URL of the remote MCP server
|
* URL of the remote MCP server
|
||||||
*/
|
*/
|
||||||
@@ -1010,7 +1074,7 @@ export type _Error = {
|
|||||||
|
|
||||||
export type TextPartInput = {
|
export type TextPartInput = {
|
||||||
id?: string
|
id?: string
|
||||||
type: string
|
type: "text"
|
||||||
text: string
|
text: string
|
||||||
synthetic?: boolean
|
synthetic?: boolean
|
||||||
time?: {
|
time?: {
|
||||||
@@ -1021,7 +1085,7 @@ export type TextPartInput = {
|
|||||||
|
|
||||||
export type FilePartInput = {
|
export type FilePartInput = {
|
||||||
id?: string
|
id?: string
|
||||||
type: string
|
type: "file"
|
||||||
mime: string
|
mime: string
|
||||||
filename?: string
|
filename?: string
|
||||||
url: string
|
url: string
|
||||||
@@ -1030,7 +1094,7 @@ export type FilePartInput = {
|
|||||||
|
|
||||||
export type AgentPartInput = {
|
export type AgentPartInput = {
|
||||||
id?: string
|
id?: string
|
||||||
type: string
|
type: "agent"
|
||||||
name: string
|
name: string
|
||||||
source?: {
|
source?: {
|
||||||
value: string
|
value: string
|
||||||
@@ -1058,9 +1122,17 @@ export type File = {
|
|||||||
export type Agent = {
|
export type Agent = {
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
mode: string
|
mode: "subagent" | "primary" | "all"
|
||||||
|
builtIn: boolean
|
||||||
topP?: number
|
topP?: number
|
||||||
temperature?: number
|
temperature?: number
|
||||||
|
permission: {
|
||||||
|
edit: "ask" | "allow" | "deny"
|
||||||
|
bash: {
|
||||||
|
[key: string]: "ask" | "allow" | "deny"
|
||||||
|
}
|
||||||
|
webfetch?: "ask" | "allow" | "deny"
|
||||||
|
}
|
||||||
model?: {
|
model?: {
|
||||||
modelID: string
|
modelID: string
|
||||||
providerID: string
|
providerID: string
|
||||||
@@ -1074,6 +1146,35 @@ export type Agent = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Auth =
|
||||||
|
| ({
|
||||||
|
type: "oauth"
|
||||||
|
} & OAuth)
|
||||||
|
| ({
|
||||||
|
type: "api"
|
||||||
|
} & ApiAuth)
|
||||||
|
| ({
|
||||||
|
type: "wellknown"
|
||||||
|
} & WellKnownAuth)
|
||||||
|
|
||||||
|
export type OAuth = {
|
||||||
|
type: "oauth"
|
||||||
|
refresh: string
|
||||||
|
access: string
|
||||||
|
expires: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiAuth = {
|
||||||
|
type: "api"
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WellKnownAuth = {
|
||||||
|
type: "wellknown"
|
||||||
|
key: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
export type EventSubscribeData = {
|
export type EventSubscribeData = {
|
||||||
body?: never
|
body?: never
|
||||||
path?: never
|
path?: never
|
||||||
@@ -1155,7 +1256,10 @@ export type SessionListResponses = {
|
|||||||
export type SessionListResponse = SessionListResponses[keyof SessionListResponses]
|
export type SessionListResponse = SessionListResponses[keyof SessionListResponses]
|
||||||
|
|
||||||
export type SessionCreateData = {
|
export type SessionCreateData = {
|
||||||
body?: never
|
body?: {
|
||||||
|
parentID?: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
path?: never
|
path?: never
|
||||||
query?: never
|
query?: never
|
||||||
url: "/session"
|
url: "/session"
|
||||||
@@ -1215,6 +1319,44 @@ export type SessionGetResponses = {
|
|||||||
|
|
||||||
export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses]
|
export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses]
|
||||||
|
|
||||||
|
export type SessionUpdateData = {
|
||||||
|
body?: {
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
path: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
query?: never
|
||||||
|
url: "/session/{id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionUpdateResponses = {
|
||||||
|
/**
|
||||||
|
* Successfully updated session
|
||||||
|
*/
|
||||||
|
200: Session
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionUpdateResponse = SessionUpdateResponses[keyof SessionUpdateResponses]
|
||||||
|
|
||||||
|
export type SessionChildrenData = {
|
||||||
|
body?: never
|
||||||
|
path: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
query?: never
|
||||||
|
url: "/session/{id}/children"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionChildrenResponses = {
|
||||||
|
/**
|
||||||
|
* List of children
|
||||||
|
*/
|
||||||
|
200: Array<Session>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionChildrenResponse = SessionChildrenResponses[keyof SessionChildrenResponses]
|
||||||
|
|
||||||
export type SessionInitData = {
|
export type SessionInitData = {
|
||||||
body?: {
|
body?: {
|
||||||
messageID: string
|
messageID: string
|
||||||
@@ -1411,6 +1553,30 @@ export type SessionMessageResponses = {
|
|||||||
|
|
||||||
export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses]
|
export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses]
|
||||||
|
|
||||||
|
export type SessionShellData = {
|
||||||
|
body?: {
|
||||||
|
agent: string
|
||||||
|
command: string
|
||||||
|
}
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Session ID
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
query?: never
|
||||||
|
url: "/session/{id}/shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionShellResponses = {
|
||||||
|
/**
|
||||||
|
* Created message
|
||||||
|
*/
|
||||||
|
200: AssistantMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses]
|
||||||
|
|
||||||
export type SessionRevertData = {
|
export type SessionRevertData = {
|
||||||
body?: {
|
body?: {
|
||||||
messageID: string
|
messageID: string
|
||||||
@@ -1783,6 +1949,53 @@ export type TuiExecuteCommandResponses = {
|
|||||||
|
|
||||||
export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses]
|
export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses]
|
||||||
|
|
||||||
|
export type TuiShowToastData = {
|
||||||
|
body?: {
|
||||||
|
title?: string
|
||||||
|
message: string
|
||||||
|
variant: "info" | "success" | "warning" | "error"
|
||||||
|
}
|
||||||
|
path?: never
|
||||||
|
query?: never
|
||||||
|
url: "/tui/show-toast"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TuiShowToastResponses = {
|
||||||
|
/**
|
||||||
|
* Toast notification shown successfully
|
||||||
|
*/
|
||||||
|
200: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses]
|
||||||
|
|
||||||
|
export type AuthSetData = {
|
||||||
|
body?: Auth
|
||||||
|
path: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
query?: never
|
||||||
|
url: "/auth/{id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthSetErrors = {
|
||||||
|
/**
|
||||||
|
* Bad request
|
||||||
|
*/
|
||||||
|
400: _Error
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthSetError = AuthSetErrors[keyof AuthSetErrors]
|
||||||
|
|
||||||
|
export type AuthSetResponses = {
|
||||||
|
/**
|
||||||
|
* Successfully set authentication credentials
|
||||||
|
*/
|
||||||
|
200: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses]
|
||||||
|
|
||||||
export type ClientOptions = {
|
export type ClientOptions = {
|
||||||
baseUrl: `${string}://${string}` | (string & {})
|
baseUrl: `${string}://${string}` | (string & {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,34 @@ import { createClient } from "./gen/client/client.js"
|
|||||||
import { type Config } from "./gen/client/types.js"
|
import { type Config } from "./gen/client/types.js"
|
||||||
import { OpencodeClient } from "./gen/sdk.gen.js"
|
import { OpencodeClient } from "./gen/sdk.gen.js"
|
||||||
export * from "./gen/types.gen.js"
|
export * from "./gen/types.gen.js"
|
||||||
|
import { spawn } from "child_process"
|
||||||
|
|
||||||
export function createOpencodeClient(config?: Config) {
|
export function createOpencodeClient(config?: Config) {
|
||||||
const client = createClient(config)
|
const client = createClient(config)
|
||||||
return new OpencodeClient({ client })
|
return new OpencodeClient({ client })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ServerConfig = {
|
||||||
|
host?: string
|
||||||
|
port?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createOpencodeServer(config?: ServerConfig) {
|
||||||
|
config = Object.assign(
|
||||||
|
{
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 4096,
|
||||||
|
},
|
||||||
|
config ?? {},
|
||||||
|
)
|
||||||
|
|
||||||
|
const proc = spawn(`opencode`, [`serve`, `--host=${config.host}`, `--port=${config.port}`])
|
||||||
|
const url = `http://${config.host}:${config.port}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
close() {
|
||||||
|
proc.kill()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ console.log("=== Generating Stainless SDK ===")
|
|||||||
console.log(process.cwd())
|
console.log(process.cwd())
|
||||||
|
|
||||||
await $`rm -rf go`
|
await $`rm -rf go`
|
||||||
await $`bun run ../../opencode/src/index.ts generate > openapi.json`
|
await $`bun run --conditions=development ../../opencode/src/index.ts generate > openapi.json`
|
||||||
await $`stl builds create --branch dev --pull --allow-empty --+target go`
|
await $`stl builds create --branch dev --pull --allow-empty --+target go`
|
||||||
|
|
||||||
await $`rm -rf ../go`
|
await $`rm -rf ../go`
|
||||||
|
|||||||
@@ -113,7 +113,9 @@ resources:
|
|||||||
toolStateError: ToolStateError
|
toolStateError: ToolStateError
|
||||||
|
|
||||||
methods:
|
methods:
|
||||||
|
get: get /session/{id}
|
||||||
list: get /session
|
list: get /session
|
||||||
|
children: get /session/{id}/children
|
||||||
create: post /session
|
create: post /session
|
||||||
delete: delete /session/{id}
|
delete: delete /session/{id}
|
||||||
init: post /session/{id}/init
|
init: post /session/{id}/init
|
||||||
@@ -124,6 +126,8 @@ resources:
|
|||||||
message: get /session/{id}/message/{messageID}
|
message: get /session/{id}/message/{messageID}
|
||||||
messages: get /session/{id}/message
|
messages: get /session/{id}/message
|
||||||
chat: post /session/{id}/message
|
chat: post /session/{id}/message
|
||||||
|
shell: post /session/{id}/shell
|
||||||
|
update: patch /session/{id}
|
||||||
revert: post /session/{id}/revert
|
revert: post /session/{id}/revert
|
||||||
unrevert: post /session/{id}/unrevert
|
unrevert: post /session/{id}/unrevert
|
||||||
|
|
||||||
@@ -144,6 +148,7 @@ resources:
|
|||||||
openThemes: post /tui/open-themes
|
openThemes: post /tui/open-themes
|
||||||
openModels: post /tui/open-models
|
openModels: post /tui/open-models
|
||||||
executeCommand: post /tui/execute-command
|
executeCommand: post /tui/execute-command
|
||||||
|
showToast: post /tui/show-toast
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
disable_mock_tests: true
|
disable_mock_tests: true
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ type App struct {
|
|||||||
InitialSession *string
|
InitialSession *string
|
||||||
compactCancel context.CancelFunc
|
compactCancel context.CancelFunc
|
||||||
IsLeaderSequence bool
|
IsLeaderSequence bool
|
||||||
|
IsBashMode bool
|
||||||
|
ScrollSpeed int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Agent() *opencode.Agent {
|
func (a *App) Agent() *opencode.Agent {
|
||||||
@@ -71,12 +73,17 @@ type ModelSelectedMsg struct {
|
|||||||
Provider opencode.Provider
|
Provider opencode.Provider
|
||||||
Model opencode.Model
|
Model opencode.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentSelectedMsg struct {
|
type AgentSelectedMsg struct {
|
||||||
Agent opencode.Agent
|
AgentName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionClearedMsg struct{}
|
type SessionClearedMsg struct{}
|
||||||
type CompactSessionMsg struct{}
|
type CompactSessionMsg struct{}
|
||||||
type SendPrompt = Prompt
|
type SendPrompt = Prompt
|
||||||
|
type SendShell = struct {
|
||||||
|
Command string
|
||||||
|
}
|
||||||
type SetEditorContentMsg struct {
|
type SetEditorContentMsg struct {
|
||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
@@ -192,6 +199,7 @@ func New(
|
|||||||
InitialPrompt: initialPrompt,
|
InitialPrompt: initialPrompt,
|
||||||
InitialAgent: initialAgent,
|
InitialAgent: initialAgent,
|
||||||
InitialSession: initialSession,
|
InitialSession: initialSession,
|
||||||
|
ScrollSpeed: int(configInfo.Tui.ScrollSpeed),
|
||||||
}
|
}
|
||||||
|
|
||||||
return app, nil
|
return app, nil
|
||||||
@@ -272,6 +280,7 @@ func (a *App) cycleMode(forward bool) (*App, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.State.Agent = a.Agent().Name
|
a.State.Agent = a.Agent().Name
|
||||||
|
a.State.UpdateAgentUsage(a.Agent().Name)
|
||||||
return a, a.SaveState()
|
return a, a.SaveState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +292,7 @@ func (a *App) SwitchAgentReverse() (*App, tea.Cmd) {
|
|||||||
return a.cycleMode(false)
|
return a.cycleMode(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CycleRecentModel() (*App, tea.Cmd) {
|
func (a *App) cycleRecentModel(forward bool) (*App, tea.Cmd) {
|
||||||
recentModels := a.State.RecentlyUsedModels
|
recentModels := a.State.RecentlyUsedModels
|
||||||
if len(recentModels) > 5 {
|
if len(recentModels) > 5 {
|
||||||
recentModels = recentModels[:5]
|
recentModels = recentModels[:5]
|
||||||
@@ -292,30 +301,101 @@ func (a *App) CycleRecentModel() (*App, tea.Cmd) {
|
|||||||
return a, toast.NewInfoToast("Need at least 2 recent models to cycle")
|
return a, toast.NewInfoToast("Need at least 2 recent models to cycle")
|
||||||
}
|
}
|
||||||
nextIndex := 0
|
nextIndex := 0
|
||||||
|
prevIndex := 0
|
||||||
for i, recentModel := range recentModels {
|
for i, recentModel := range recentModels {
|
||||||
if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID && recentModel.ModelID == a.Model.ID {
|
if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID &&
|
||||||
|
recentModel.ModelID == a.Model.ID {
|
||||||
nextIndex = (i + 1) % len(recentModels)
|
nextIndex = (i + 1) % len(recentModels)
|
||||||
|
prevIndex = (i - 1 + len(recentModels)) % len(recentModels)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
targetIndex := nextIndex
|
||||||
|
if !forward {
|
||||||
|
targetIndex = prevIndex
|
||||||
|
}
|
||||||
for range recentModels {
|
for range recentModels {
|
||||||
currentRecentModel := recentModels[nextIndex%len(recentModels)]
|
currentRecentModel := recentModels[targetIndex%len(recentModels)]
|
||||||
provider, model := findModelByProviderAndModelID(a.Providers, currentRecentModel.ProviderID, currentRecentModel.ModelID)
|
provider, model := findModelByProviderAndModelID(
|
||||||
|
a.Providers,
|
||||||
|
currentRecentModel.ProviderID,
|
||||||
|
currentRecentModel.ModelID,
|
||||||
|
)
|
||||||
if provider != nil && model != nil {
|
if provider != nil && model != nil {
|
||||||
a.Provider, a.Model = provider, model
|
a.Provider, a.Model = provider, model
|
||||||
a.State.AgentModel[a.Agent().Name] = AgentModel{ProviderID: provider.ID, ModelID: model.ID}
|
a.State.AgentModel[a.Agent().Name] = AgentModel{
|
||||||
return a, tea.Sequence(a.SaveState(), toast.NewSuccessToast(fmt.Sprintf("Switched to %s (%s)", model.Name, provider.Name)))
|
ProviderID: provider.ID,
|
||||||
|
ModelID: model.ID,
|
||||||
|
}
|
||||||
|
return a, tea.Sequence(
|
||||||
|
a.SaveState(),
|
||||||
|
toast.NewSuccessToast(
|
||||||
|
fmt.Sprintf("Switched to %s (%s)", model.Name, provider.Name),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
recentModels = append(recentModels[:nextIndex%len(recentModels)], recentModels[nextIndex%len(recentModels)+1:]...)
|
recentModels = append(
|
||||||
|
recentModels[:targetIndex%len(recentModels)],
|
||||||
|
recentModels[targetIndex%len(recentModels)+1:]...)
|
||||||
if len(recentModels) < 2 {
|
if len(recentModels) < 2 {
|
||||||
a.State.RecentlyUsedModels = recentModels
|
a.State.RecentlyUsedModels = recentModels
|
||||||
return a, tea.Sequence(a.SaveState(), toast.NewInfoToast("Not enough valid recent models to cycle"))
|
return a, tea.Sequence(
|
||||||
|
a.SaveState(),
|
||||||
|
toast.NewInfoToast("Not enough valid recent models to cycle"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.State.RecentlyUsedModels = recentModels
|
a.State.RecentlyUsedModels = recentModels
|
||||||
return a, toast.NewErrorToast("Recent model not found")
|
return a, toast.NewErrorToast("Recent model not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) CycleRecentModel() (*App, tea.Cmd) {
|
||||||
|
return a.cycleRecentModel(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) CycleRecentModelReverse() (*App, tea.Cmd) {
|
||||||
|
return a.cycleRecentModel(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SwitchToAgent(agentName string) (*App, tea.Cmd) {
|
||||||
|
// Find the agent index by name
|
||||||
|
for i, agent := range a.Agents {
|
||||||
|
if agent.Name == agentName {
|
||||||
|
a.AgentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up model for the new agent
|
||||||
|
modelID := a.Agent().Model.ModelID
|
||||||
|
providerID := a.Agent().Model.ProviderID
|
||||||
|
if modelID == "" {
|
||||||
|
if model, ok := a.State.AgentModel[a.Agent().Name]; ok {
|
||||||
|
modelID = model.ModelID
|
||||||
|
providerID = model.ProviderID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if modelID != "" {
|
||||||
|
for _, provider := range a.Providers {
|
||||||
|
if provider.ID == providerID {
|
||||||
|
a.Provider = &provider
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
if model.ID == modelID {
|
||||||
|
a.Model = &model
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.State.Agent = a.Agent().Name
|
||||||
|
a.State.UpdateAgentUsage(agentName)
|
||||||
|
return a, a.SaveState()
|
||||||
|
}
|
||||||
|
|
||||||
// findModelByFullID finds a model by its full ID in the format "provider/model"
|
// findModelByFullID finds a model by its full ID in the format "provider/model"
|
||||||
func findModelByFullID(
|
func findModelByFullID(
|
||||||
providers []opencode.Provider,
|
providers []opencode.Provider,
|
||||||
@@ -422,10 +502,19 @@ func (a *App) InitializeProvider() tea.Cmd {
|
|||||||
|
|
||||||
// Priority 3: Current agent's preferred model
|
// Priority 3: Current agent's preferred model
|
||||||
if selectedProvider == nil && a.Agent().Model.ModelID != "" {
|
if selectedProvider == nil && a.Agent().Model.ModelID != "" {
|
||||||
if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil && model != nil {
|
if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil &&
|
||||||
|
model != nil {
|
||||||
selectedProvider = provider
|
selectedProvider = provider
|
||||||
selectedModel = model
|
selectedModel = model
|
||||||
slog.Debug("Selected model from current agent", "provider", provider.ID, "model", model.ID, "agent", a.Agent().Name)
|
slog.Debug(
|
||||||
|
"Selected model from current agent",
|
||||||
|
"provider",
|
||||||
|
provider.ID,
|
||||||
|
"model",
|
||||||
|
model.ID,
|
||||||
|
"agent",
|
||||||
|
a.Agent().Name,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
slog.Debug("Agent model not found", "provider", a.Agent().Model.ProviderID, "model", a.Agent().Model.ModelID, "agent", a.Agent().Name)
|
slog.Debug("Agent model not found", "provider", a.Agent().Model.ProviderID, "model", a.Agent().Model.ModelID, "agent", a.Agent().Name)
|
||||||
}
|
}
|
||||||
@@ -558,7 +647,7 @@ func (a *App) IsBusy() bool {
|
|||||||
if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok {
|
if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok {
|
||||||
return casted.Time.Completed == 0
|
return casted.Time.Completed == 0
|
||||||
}
|
}
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SaveState() tea.Cmd {
|
func (a *App) SaveState() tea.Cmd {
|
||||||
@@ -638,7 +727,7 @@ func (a *App) MarkProjectInitialized(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
|
func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
|
||||||
session, err := a.Client.Session.New(ctx)
|
session, err := a.Client.Session.New(ctx, opencode.SessionNewParams{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -682,6 +771,38 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
|
|||||||
return a, tea.Batch(cmds...)
|
return a, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
if a.Session.ID == "" {
|
||||||
|
session, err := a.CreateSession(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return a, toast.NewErrorToast(err.Error())
|
||||||
|
}
|
||||||
|
a.Session = session
|
||||||
|
cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds = append(cmds, func() tea.Msg {
|
||||||
|
_, err := a.Client.Session.Shell(
|
||||||
|
context.Background(),
|
||||||
|
a.Session.ID,
|
||||||
|
opencode.SessionShellParams{
|
||||||
|
Agent: opencode.F(a.Agent().Name),
|
||||||
|
Command: opencode.F(command),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to submit shell command", "error", err)
|
||||||
|
return toast.NewErrorToast("Failed to submit shell command")()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// The actual response will come through SSE
|
||||||
|
// For now, just return success
|
||||||
|
return a, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
||||||
// Cancel any running compact operation
|
// Cancel any running compact operation
|
||||||
if a.compactCancel != nil {
|
if a.compactCancel != nil {
|
||||||
@@ -718,6 +839,17 @@ func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) UpdateSession(ctx context.Context, sessionID string, title string) error {
|
||||||
|
_, err := a.Client.Session.Update(ctx, sessionID, opencode.SessionUpdateParams{
|
||||||
|
Title: opencode.F(title),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to update session", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
|
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
|
||||||
response, err := a.Client.Session.Messages(ctx, sessionId)
|
response, err := a.Client.Session.Messages(ctx, sessionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ type ModelUsage struct {
|
|||||||
LastUsed time.Time `toml:"last_used"`
|
LastUsed time.Time `toml:"last_used"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentUsage struct {
|
||||||
|
AgentName string `toml:"agent_name"`
|
||||||
|
LastUsed time.Time `toml:"last_used"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentModel struct {
|
type AgentModel struct {
|
||||||
ProviderID string `toml:"provider_id"`
|
ProviderID string `toml:"provider_id"`
|
||||||
ModelID string `toml:"model_id"`
|
ModelID string `toml:"model_id"`
|
||||||
@@ -23,14 +28,12 @@ type AgentModel struct {
|
|||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
Theme string `toml:"theme"`
|
Theme string `toml:"theme"`
|
||||||
ScrollSpeed *int `toml:"scroll_speed"`
|
|
||||||
AgentModel map[string]AgentModel `toml:"agent_model"`
|
AgentModel map[string]AgentModel `toml:"agent_model"`
|
||||||
Provider string `toml:"provider"`
|
Provider string `toml:"provider"`
|
||||||
Model string `toml:"model"`
|
Model string `toml:"model"`
|
||||||
Agent string `toml:"agent"`
|
Agent string `toml:"agent"`
|
||||||
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
|
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
|
||||||
MessagesRight bool `toml:"messages_right"`
|
RecentlyUsedAgents []AgentUsage `toml:"recently_used_agents"`
|
||||||
SplitDiff bool `toml:"split_diff"`
|
|
||||||
MessageHistory []Prompt `toml:"message_history"`
|
MessageHistory []Prompt `toml:"message_history"`
|
||||||
ShowToolDetails *bool `toml:"show_tool_details"`
|
ShowToolDetails *bool `toml:"show_tool_details"`
|
||||||
ShowThinkingBlocks *bool `toml:"show_thinking_blocks"`
|
ShowThinkingBlocks *bool `toml:"show_thinking_blocks"`
|
||||||
@@ -42,6 +45,7 @@ func NewState() *State {
|
|||||||
Agent: "build",
|
Agent: "build",
|
||||||
AgentModel: make(map[string]AgentModel),
|
AgentModel: make(map[string]AgentModel),
|
||||||
RecentlyUsedModels: make([]ModelUsage, 0),
|
RecentlyUsedModels: make([]ModelUsage, 0),
|
||||||
|
RecentlyUsedAgents: make([]AgentUsage, 0),
|
||||||
MessageHistory: make([]Prompt, 0),
|
MessageHistory: make([]Prompt, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +87,42 @@ func (s *State) RemoveModelFromRecentlyUsed(providerID, modelID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAgentUsage updates the recently used agents list with the specified agent
|
||||||
|
func (s *State) UpdateAgentUsage(agentName string) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Check if this agent is already in the list
|
||||||
|
for i, usage := range s.RecentlyUsedAgents {
|
||||||
|
if usage.AgentName == agentName {
|
||||||
|
s.RecentlyUsedAgents[i].LastUsed = now
|
||||||
|
usage := s.RecentlyUsedAgents[i]
|
||||||
|
copy(s.RecentlyUsedAgents[1:i+1], s.RecentlyUsedAgents[0:i])
|
||||||
|
s.RecentlyUsedAgents[0] = usage
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newUsage := AgentUsage{
|
||||||
|
AgentName: agentName,
|
||||||
|
LastUsed: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepend to slice and limit to last 20 entries
|
||||||
|
s.RecentlyUsedAgents = append([]AgentUsage{newUsage}, s.RecentlyUsedAgents...)
|
||||||
|
if len(s.RecentlyUsedAgents) > 20 {
|
||||||
|
s.RecentlyUsedAgents = s.RecentlyUsedAgents[:20]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) RemoveAgentFromRecentlyUsed(agentName string) {
|
||||||
|
for i, usage := range s.RecentlyUsedAgents {
|
||||||
|
if usage.AgentName == agentName {
|
||||||
|
s.RecentlyUsedAgents = append(s.RecentlyUsedAgents[:i], s.RecentlyUsedAgents[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *State) AddPromptToHistory(prompt Prompt) {
|
func (s *State) AddPromptToHistory(prompt Prompt) {
|
||||||
s.MessageHistory = append([]Prompt{prompt}, s.MessageHistory...)
|
s.MessageHistory = append([]Prompt{prompt}, s.MessageHistory...)
|
||||||
if len(s.MessageHistory) > 50 {
|
if len(s.MessageHistory) > 50 {
|
||||||
|
|||||||
@@ -107,45 +107,52 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AppHelpCommand CommandName = "app_help"
|
|
||||||
SwitchAgentCommand CommandName = "switch_agent"
|
SessionChildCycleCommand CommandName = "session_child_cycle"
|
||||||
SwitchAgentReverseCommand CommandName = "switch_agent_reverse"
|
SessionChildCycleReverseCommand CommandName = "session_child_cycle_reverse"
|
||||||
EditorOpenCommand CommandName = "editor_open"
|
ModelCycleRecentReverseCommand CommandName = "model_cycle_recent_reverse"
|
||||||
SessionNewCommand CommandName = "session_new"
|
AgentCycleCommand CommandName = "agent_cycle"
|
||||||
SessionListCommand CommandName = "session_list"
|
AgentCycleReverseCommand CommandName = "agent_cycle_reverse"
|
||||||
SessionShareCommand CommandName = "session_share"
|
AppHelpCommand CommandName = "app_help"
|
||||||
SessionUnshareCommand CommandName = "session_unshare"
|
SwitchAgentCommand CommandName = "switch_agent"
|
||||||
SessionInterruptCommand CommandName = "session_interrupt"
|
SwitchAgentReverseCommand CommandName = "switch_agent_reverse"
|
||||||
SessionCompactCommand CommandName = "session_compact"
|
EditorOpenCommand CommandName = "editor_open"
|
||||||
SessionExportCommand CommandName = "session_export"
|
SessionNewCommand CommandName = "session_new"
|
||||||
ToolDetailsCommand CommandName = "tool_details"
|
SessionListCommand CommandName = "session_list"
|
||||||
ThinkingBlocksCommand CommandName = "thinking_blocks"
|
SessionNavigationCommand CommandName = "session_navigation"
|
||||||
ModelListCommand CommandName = "model_list"
|
SessionShareCommand CommandName = "session_share"
|
||||||
AgentListCommand CommandName = "agent_list"
|
SessionUnshareCommand CommandName = "session_unshare"
|
||||||
ModelCycleRecentCommand CommandName = "model_cycle_recent"
|
SessionInterruptCommand CommandName = "session_interrupt"
|
||||||
ThemeListCommand CommandName = "theme_list"
|
SessionCompactCommand CommandName = "session_compact"
|
||||||
FileListCommand CommandName = "file_list"
|
SessionExportCommand CommandName = "session_export"
|
||||||
FileCloseCommand CommandName = "file_close"
|
ToolDetailsCommand CommandName = "tool_details"
|
||||||
FileSearchCommand CommandName = "file_search"
|
ThinkingBlocksCommand CommandName = "thinking_blocks"
|
||||||
FileDiffToggleCommand CommandName = "file_diff_toggle"
|
ModelListCommand CommandName = "model_list"
|
||||||
ProjectInitCommand CommandName = "project_init"
|
AgentListCommand CommandName = "agent_list"
|
||||||
InputClearCommand CommandName = "input_clear"
|
ModelCycleRecentCommand CommandName = "model_cycle_recent"
|
||||||
InputPasteCommand CommandName = "input_paste"
|
ThemeListCommand CommandName = "theme_list"
|
||||||
InputSubmitCommand CommandName = "input_submit"
|
FileListCommand CommandName = "file_list"
|
||||||
InputNewlineCommand CommandName = "input_newline"
|
FileCloseCommand CommandName = "file_close"
|
||||||
MessagesPageUpCommand CommandName = "messages_page_up"
|
FileSearchCommand CommandName = "file_search"
|
||||||
MessagesPageDownCommand CommandName = "messages_page_down"
|
FileDiffToggleCommand CommandName = "file_diff_toggle"
|
||||||
MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
|
ProjectInitCommand CommandName = "project_init"
|
||||||
MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
|
InputClearCommand CommandName = "input_clear"
|
||||||
MessagesPreviousCommand CommandName = "messages_previous"
|
InputPasteCommand CommandName = "input_paste"
|
||||||
MessagesNextCommand CommandName = "messages_next"
|
InputSubmitCommand CommandName = "input_submit"
|
||||||
MessagesFirstCommand CommandName = "messages_first"
|
InputNewlineCommand CommandName = "input_newline"
|
||||||
MessagesLastCommand CommandName = "messages_last"
|
MessagesPageUpCommand CommandName = "messages_page_up"
|
||||||
MessagesLayoutToggleCommand CommandName = "messages_layout_toggle"
|
MessagesPageDownCommand CommandName = "messages_page_down"
|
||||||
MessagesCopyCommand CommandName = "messages_copy"
|
MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
|
||||||
MessagesUndoCommand CommandName = "messages_undo"
|
MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
|
||||||
MessagesRedoCommand CommandName = "messages_redo"
|
MessagesPreviousCommand CommandName = "messages_previous"
|
||||||
AppExitCommand CommandName = "app_exit"
|
MessagesNextCommand CommandName = "messages_next"
|
||||||
|
MessagesFirstCommand CommandName = "messages_first"
|
||||||
|
MessagesLastCommand CommandName = "messages_last"
|
||||||
|
MessagesLayoutToggleCommand CommandName = "messages_layout_toggle"
|
||||||
|
MessagesCopyCommand CommandName = "messages_copy"
|
||||||
|
MessagesUndoCommand CommandName = "messages_undo"
|
||||||
|
MessagesRedoCommand CommandName = "messages_redo"
|
||||||
|
AppExitCommand CommandName = "app_exit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
|
func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
|
||||||
@@ -184,16 +191,6 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Keybindings: parseBindings("<leader>h"),
|
Keybindings: parseBindings("<leader>h"),
|
||||||
Trigger: []string{"help"},
|
Trigger: []string{"help"},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: SwitchAgentCommand,
|
|
||||||
Description: "next agent",
|
|
||||||
Keybindings: parseBindings("tab"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: SwitchAgentReverseCommand,
|
|
||||||
Description: "previous agent",
|
|
||||||
Keybindings: parseBindings("shift+tab"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: EditorOpenCommand,
|
Name: EditorOpenCommand,
|
||||||
Description: "open editor",
|
Description: "open editor",
|
||||||
@@ -218,6 +215,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Keybindings: parseBindings("<leader>l"),
|
Keybindings: parseBindings("<leader>l"),
|
||||||
Trigger: []string{"sessions", "resume", "continue"},
|
Trigger: []string{"sessions", "resume", "continue"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: SessionNavigationCommand,
|
||||||
|
Description: "jump to message",
|
||||||
|
Keybindings: parseBindings("<leader>g"),
|
||||||
|
Trigger: []string{"jump", "goto", "navigate"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: SessionShareCommand,
|
Name: SessionShareCommand,
|
||||||
Description: "share session",
|
Description: "share session",
|
||||||
@@ -240,6 +243,16 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Keybindings: parseBindings("<leader>c"),
|
Keybindings: parseBindings("<leader>c"),
|
||||||
Trigger: []string{"compact", "summarize"},
|
Trigger: []string{"compact", "summarize"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: SessionChildCycleCommand,
|
||||||
|
Description: "cycle to next child session",
|
||||||
|
Keybindings: parseBindings("ctrl+right"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SessionChildCycleReverseCommand,
|
||||||
|
Description: "cycle to previous child session",
|
||||||
|
Keybindings: parseBindings("ctrl+left"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: ToolDetailsCommand,
|
Name: ToolDetailsCommand,
|
||||||
Description: "toggle tool details",
|
Description: "toggle tool details",
|
||||||
@@ -258,6 +271,16 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Keybindings: parseBindings("<leader>m"),
|
Keybindings: parseBindings("<leader>m"),
|
||||||
Trigger: []string{"models"},
|
Trigger: []string{"models"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: ModelCycleRecentCommand,
|
||||||
|
Description: "next recent model",
|
||||||
|
Keybindings: parseBindings("f2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: ModelCycleRecentReverseCommand,
|
||||||
|
Description: "previous recent model",
|
||||||
|
Keybindings: parseBindings("shift+f2"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: AgentListCommand,
|
Name: AgentListCommand,
|
||||||
Description: "list agents",
|
Description: "list agents",
|
||||||
@@ -265,9 +288,14 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Trigger: []string{"agents"},
|
Trigger: []string{"agents"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: ModelCycleRecentCommand,
|
Name: AgentCycleCommand,
|
||||||
Description: "cycle recent models",
|
Description: "next agent",
|
||||||
Keybindings: parseBindings("f2"),
|
Keybindings: parseBindings("tab"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: AgentCycleReverseCommand,
|
||||||
|
Description: "previous agent",
|
||||||
|
Keybindings: parseBindings("shift+tab"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: ThemeListCommand,
|
Name: ThemeListCommand,
|
||||||
@@ -275,27 +303,6 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Keybindings: parseBindings("<leader>t"),
|
Keybindings: parseBindings("<leader>t"),
|
||||||
Trigger: []string{"themes"},
|
Trigger: []string{"themes"},
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// Name: FileListCommand,
|
|
||||||
// Description: "list files",
|
|
||||||
// Keybindings: parseBindings("<leader>f"),
|
|
||||||
// Trigger: []string{"files"},
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
Name: FileCloseCommand,
|
|
||||||
Description: "close file",
|
|
||||||
Keybindings: parseBindings("esc"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: FileSearchCommand,
|
|
||||||
Description: "search file",
|
|
||||||
Keybindings: parseBindings("<leader>/"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: FileDiffToggleCommand,
|
|
||||||
Description: "split/unified diff",
|
|
||||||
Keybindings: parseBindings("<leader>v"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: ProjectInitCommand,
|
Name: ProjectInitCommand,
|
||||||
Description: "create/update AGENTS.md",
|
Description: "create/update AGENTS.md",
|
||||||
@@ -342,16 +349,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Description: "half page down",
|
Description: "half page down",
|
||||||
Keybindings: parseBindings("ctrl+alt+d"),
|
Keybindings: parseBindings("ctrl+alt+d"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: MessagesPreviousCommand,
|
|
||||||
Description: "previous message",
|
|
||||||
Keybindings: parseBindings("ctrl+up"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: MessagesNextCommand,
|
|
||||||
Description: "next message",
|
|
||||||
Keybindings: parseBindings("ctrl+down"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: MessagesFirstCommand,
|
Name: MessagesFirstCommand,
|
||||||
Description: "first message",
|
Description: "first message",
|
||||||
@@ -362,11 +360,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Description: "last message",
|
Description: "last message",
|
||||||
Keybindings: parseBindings("ctrl+alt+g"),
|
Keybindings: parseBindings("ctrl+alt+g"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: MessagesLayoutToggleCommand,
|
|
||||||
Description: "toggle layout",
|
|
||||||
Keybindings: parseBindings("<leader>p"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: MessagesCopyCommand,
|
Name: MessagesCopyCommand,
|
||||||
Description: "copy message",
|
Description: "copy message",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type EditorComponent interface {
|
|||||||
tea.Model
|
tea.Model
|
||||||
tea.ViewModel
|
tea.ViewModel
|
||||||
Content() string
|
Content() string
|
||||||
|
Cursor() *tea.Cursor
|
||||||
Lines() int
|
Lines() int
|
||||||
Value() string
|
Value() string
|
||||||
Length() int
|
Length() int
|
||||||
@@ -38,6 +39,7 @@ type EditorComponent interface {
|
|||||||
Focus() (tea.Model, tea.Cmd)
|
Focus() (tea.Model, tea.Cmd)
|
||||||
Blur()
|
Blur()
|
||||||
Submit() (tea.Model, tea.Cmd)
|
Submit() (tea.Model, tea.Cmd)
|
||||||
|
SubmitBash() (tea.Model, tea.Cmd)
|
||||||
Clear() (tea.Model, tea.Cmd)
|
Clear() (tea.Model, tea.Cmd)
|
||||||
Paste() (tea.Model, tea.Cmd)
|
Paste() (tea.Model, tea.Cmd)
|
||||||
Newline() (tea.Model, tea.Cmd)
|
Newline() (tea.Model, tea.Cmd)
|
||||||
@@ -341,6 +343,14 @@ func (m *editorComponent) Content() string {
|
|||||||
Padding(0, 0, 0, 1).
|
Padding(0, 0, 0, 1).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
prompt := promptStyle.Render(">")
|
prompt := promptStyle.Render(">")
|
||||||
|
borderForeground := t.Border()
|
||||||
|
if m.app.IsLeaderSequence {
|
||||||
|
borderForeground = t.Accent()
|
||||||
|
}
|
||||||
|
if m.app.IsBashMode {
|
||||||
|
borderForeground = t.Secondary()
|
||||||
|
prompt = promptStyle.Render("!")
|
||||||
|
}
|
||||||
|
|
||||||
m.textarea.SetWidth(width - 6)
|
m.textarea.SetWidth(width - 6)
|
||||||
textarea := lipgloss.JoinHorizontal(
|
textarea := lipgloss.JoinHorizontal(
|
||||||
@@ -348,10 +358,6 @@ func (m *editorComponent) Content() string {
|
|||||||
prompt,
|
prompt,
|
||||||
m.textarea.View(),
|
m.textarea.View(),
|
||||||
)
|
)
|
||||||
borderForeground := t.Border()
|
|
||||||
if m.app.IsLeaderSequence {
|
|
||||||
borderForeground = t.Accent()
|
|
||||||
}
|
|
||||||
textarea = styles.NewStyle().
|
textarea = styles.NewStyle().
|
||||||
Background(t.BackgroundElement()).
|
Background(t.BackgroundElement()).
|
||||||
Width(width).
|
Width(width).
|
||||||
@@ -407,6 +413,10 @@ func (m *editorComponent) Content() string {
|
|||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) Cursor() *tea.Cursor {
|
||||||
|
return m.textarea.Cursor()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *editorComponent) View() string {
|
func (m *editorComponent) View() string {
|
||||||
width := m.width
|
width := m.width
|
||||||
if m.app.Session.ID == "" {
|
if m.app.Session.ID == "" {
|
||||||
@@ -484,6 +494,16 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
|||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) SubmitBash() (tea.Model, tea.Cmd) {
|
||||||
|
command := m.textarea.Value()
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
updated, cmd := m.Clear()
|
||||||
|
m = updated.(*editorComponent)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
cmds = append(cmds, util.CmdHandler(app.SendShell{Command: command}))
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
|
func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
|
||||||
m.textarea.Reset()
|
m.textarea.Reset()
|
||||||
m.historyIndex = -1
|
m.historyIndex = -1
|
||||||
@@ -694,6 +714,7 @@ func NewEditorComponent(app *app.App) EditorComponent {
|
|||||||
ta.Prompt = " "
|
ta.Prompt = " "
|
||||||
ta.ShowLineNumbers = false
|
ta.ShowLineNumbers = false
|
||||||
ta.CharLimit = -1
|
ta.CharLimit = -1
|
||||||
|
ta.VirtualCursor = false
|
||||||
ta = updateTextareaStyles(ta)
|
ta = updateTextareaStyles(ta)
|
||||||
|
|
||||||
m := &editorComponent{
|
m := &editorComponent{
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/muesli/reflow/truncate"
|
"github.com/muesli/reflow/truncate"
|
||||||
"github.com/sst/opencode-sdk-go"
|
"github.com/sst/opencode-sdk-go"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/commands"
|
||||||
"github.com/sst/opencode/internal/components/diff"
|
"github.com/sst/opencode/internal/components/diff"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
@@ -183,6 +184,8 @@ func renderContentBlock(
|
|||||||
if renderer.borderRight {
|
if renderer.borderRight {
|
||||||
style = style.BorderRightForeground(borderColor)
|
style = style.BorderRightForeground(borderColor)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
style = style.PaddingLeft(renderer.paddingLeft + 1).PaddingRight(renderer.paddingRight + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
content = style.Render(content)
|
content = style.Render(content)
|
||||||
@@ -209,6 +212,7 @@ func renderText(
|
|||||||
width int,
|
width int,
|
||||||
extra string,
|
extra string,
|
||||||
isThinking bool,
|
isThinking bool,
|
||||||
|
isQueued bool,
|
||||||
fileParts []opencode.FilePart,
|
fileParts []opencode.FilePart,
|
||||||
agentParts []opencode.AgentPart,
|
agentParts []opencode.AgentPart,
|
||||||
toolCalls ...opencode.ToolPart,
|
toolCalls ...opencode.ToolPart,
|
||||||
@@ -325,8 +329,14 @@ func renderText(
|
|||||||
|
|
||||||
// wrap styled text
|
// wrap styled text
|
||||||
styledText := result.String()
|
styledText := result.String()
|
||||||
wrappedText := ansi.WordwrapWc(styledText, width-6, " -")
|
styledText = strings.ReplaceAll(styledText, "-", "\u2011")
|
||||||
|
wrappedText := ansi.WordwrapWc(styledText, width-6, " ")
|
||||||
|
wrappedText = strings.ReplaceAll(wrappedText, "\u2011", "-")
|
||||||
content = base.Width(width - 6).Render(wrappedText)
|
content = base.Width(width - 6).Render(wrappedText)
|
||||||
|
if isQueued {
|
||||||
|
queuedStyle := styles.NewStyle().Background(t.Accent()).Foreground(t.BackgroundPanel()).Bold(true).Padding(0, 1)
|
||||||
|
content = queuedStyle.Render("QUEUED") + "\n\n" + content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timestamp := ts.
|
timestamp := ts.
|
||||||
@@ -396,12 +406,16 @@ func renderText(
|
|||||||
|
|
||||||
switch message.(type) {
|
switch message.(type) {
|
||||||
case opencode.UserMessage:
|
case opencode.UserMessage:
|
||||||
|
borderColor := t.Secondary()
|
||||||
|
if isQueued {
|
||||||
|
borderColor = t.Accent()
|
||||||
|
}
|
||||||
return renderContentBlock(
|
return renderContentBlock(
|
||||||
app,
|
app,
|
||||||
content,
|
content,
|
||||||
width,
|
width,
|
||||||
WithTextColor(t.Text()),
|
WithTextColor(t.Text()),
|
||||||
WithBorderColor(t.Secondary()),
|
WithBorderColor(borderColor),
|
||||||
)
|
)
|
||||||
case opencode.AssistantMessage:
|
case opencode.AssistantMessage:
|
||||||
if isThinking {
|
if isThinking {
|
||||||
@@ -466,6 +480,8 @@ func renderToolDetails(
|
|||||||
backgroundColor := t.BackgroundPanel()
|
backgroundColor := t.BackgroundPanel()
|
||||||
borderColor := t.BackgroundPanel()
|
borderColor := t.BackgroundPanel()
|
||||||
defaultStyle := styles.NewStyle().Background(backgroundColor).Width(width - 6).Render
|
defaultStyle := styles.NewStyle().Background(backgroundColor).Width(width - 6).Render
|
||||||
|
baseStyle := styles.NewStyle().Background(backgroundColor).Foreground(t.Text()).Render
|
||||||
|
mutedStyle := styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render
|
||||||
|
|
||||||
permissionContent := ""
|
permissionContent := ""
|
||||||
if permission.ID != "" {
|
if permission.ID != "" {
|
||||||
@@ -550,6 +566,17 @@ func renderToolDetails(
|
|||||||
title := renderToolTitle(toolCall, width)
|
title := renderToolTitle(toolCall, width)
|
||||||
title = style.Render(title)
|
title = style.Render(title)
|
||||||
content := title + "\n" + body
|
content := title + "\n" + body
|
||||||
|
|
||||||
|
if toolCall.State.Status == opencode.ToolPartStateStatusError {
|
||||||
|
errorStyle := styles.NewStyle().
|
||||||
|
Background(backgroundColor).
|
||||||
|
Foreground(t.Error()).
|
||||||
|
Padding(1, 2).
|
||||||
|
Width(width - 4)
|
||||||
|
errorContent := errorStyle.Render(toolCall.State.Error)
|
||||||
|
content += "\n" + errorContent
|
||||||
|
}
|
||||||
|
|
||||||
if permissionContent != "" {
|
if permissionContent != "" {
|
||||||
permissionContent = styles.NewStyle().
|
permissionContent = styles.NewStyle().
|
||||||
Background(backgroundColor).
|
Background(backgroundColor).
|
||||||
@@ -578,14 +605,15 @@ func renderToolDetails(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "bash":
|
case "bash":
|
||||||
command := toolInputMap["command"].(string)
|
if command, ok := toolInputMap["command"].(string); ok {
|
||||||
body = fmt.Sprintf("```console\n$ %s\n", command)
|
body = fmt.Sprintf("```console\n$ %s\n", command)
|
||||||
output := metadata["output"]
|
output := metadata["output"]
|
||||||
if output != nil {
|
if output != nil {
|
||||||
body += ansi.Strip(fmt.Sprintf("%s", output))
|
body += ansi.Strip(fmt.Sprintf("%s", output))
|
||||||
|
}
|
||||||
|
body += "```"
|
||||||
|
body = util.ToMarkdown(body, width, backgroundColor)
|
||||||
}
|
}
|
||||||
body += "```"
|
|
||||||
body = util.ToMarkdown(body, width, backgroundColor)
|
|
||||||
case "webfetch":
|
case "webfetch":
|
||||||
if format, ok := toolInputMap["format"].(string); ok && result != nil {
|
if format, ok := toolInputMap["format"].(string); ok && result != nil {
|
||||||
body = *result
|
body = *result
|
||||||
@@ -629,6 +657,12 @@ func renderToolDetails(
|
|||||||
steps = append(steps, step)
|
steps = append(steps, step)
|
||||||
}
|
}
|
||||||
body = strings.Join(steps, "\n")
|
body = strings.Join(steps, "\n")
|
||||||
|
|
||||||
|
body += "\n\n"
|
||||||
|
body += baseStyle(app.Keybind(commands.SessionChildCycleCommand)) +
|
||||||
|
mutedStyle(", ") +
|
||||||
|
baseStyle(app.Keybind(commands.SessionChildCycleReverseCommand)) +
|
||||||
|
mutedStyle(" navigate child sessions")
|
||||||
}
|
}
|
||||||
body = defaultStyle(body)
|
body = defaultStyle(body)
|
||||||
default:
|
default:
|
||||||
@@ -648,11 +682,17 @@ func renderToolDetails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if error != "" {
|
if error != "" {
|
||||||
body = styles.NewStyle().
|
errorContent := styles.NewStyle().
|
||||||
Width(width - 6).
|
Width(width - 6).
|
||||||
Foreground(t.Error()).
|
Foreground(t.Error()).
|
||||||
Background(backgroundColor).
|
Background(backgroundColor).
|
||||||
Render(error)
|
Render(error)
|
||||||
|
|
||||||
|
if body == "" {
|
||||||
|
body = errorContent
|
||||||
|
} else {
|
||||||
|
body += "\n\n" + errorContent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if body == "" && error == "" && result != nil {
|
if body == "" && error == "" && result != nil {
|
||||||
@@ -683,6 +723,8 @@ func renderToolDetails(
|
|||||||
|
|
||||||
func renderToolName(name string) string {
|
func renderToolName(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
|
case "bash":
|
||||||
|
return "Shell"
|
||||||
case "webfetch":
|
case "webfetch":
|
||||||
return "Fetch"
|
return "Fetch"
|
||||||
case "invalid":
|
case "invalid":
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type MessagesComponent interface {
|
|||||||
CopyLastMessage() (tea.Model, tea.Cmd)
|
CopyLastMessage() (tea.Model, tea.Cmd)
|
||||||
UndoLastMessage() (tea.Model, tea.Cmd)
|
UndoLastMessage() (tea.Model, tea.Cmd)
|
||||||
RedoLastMessage() (tea.Model, tea.Cmd)
|
RedoLastMessage() (tea.Model, tea.Cmd)
|
||||||
|
ScrollToMessage(messageID string) (tea.Model, tea.Cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
type messagesComponent struct {
|
type messagesComponent struct {
|
||||||
@@ -57,6 +58,7 @@ type messagesComponent struct {
|
|||||||
partCount int
|
partCount int
|
||||||
lineCount int
|
lineCount int
|
||||||
selection *selection
|
selection *selection
|
||||||
|
messagePositions map[string]int // map message ID to line position
|
||||||
}
|
}
|
||||||
|
|
||||||
type selection struct {
|
type selection struct {
|
||||||
@@ -180,6 +182,8 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.tail = true
|
m.tail = true
|
||||||
return m, m.renderView()
|
return m, m.renderView()
|
||||||
}
|
}
|
||||||
|
case app.SessionSelectedMsg:
|
||||||
|
m.viewport.GotoBottom()
|
||||||
case app.MessageRevertedMsg:
|
case app.MessageRevertedMsg:
|
||||||
if msg.Session.ID == m.app.Session.ID {
|
if msg.Session.ID == m.app.Session.ID {
|
||||||
m.cache.Clear()
|
m.cache.Clear()
|
||||||
@@ -203,6 +207,11 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if msg.Properties.Part.SessionID == m.app.Session.ID {
|
if msg.Properties.Part.SessionID == m.app.Session.ID {
|
||||||
cmds = append(cmds, m.renderView())
|
cmds = append(cmds, m.renderView())
|
||||||
}
|
}
|
||||||
|
case opencode.EventListResponseEventMessageRemoved:
|
||||||
|
if msg.Properties.SessionID == m.app.Session.ID {
|
||||||
|
m.cache.Clear()
|
||||||
|
cmds = append(cmds, m.renderView())
|
||||||
|
}
|
||||||
case opencode.EventListResponseEventMessagePartRemoved:
|
case opencode.EventListResponseEventMessagePartRemoved:
|
||||||
if msg.Properties.SessionID == m.app.Session.ID {
|
if msg.Properties.SessionID == m.app.Session.ID {
|
||||||
// Clear the cache when a part is removed to ensure proper re-rendering
|
// Clear the cache when a part is removed to ensure proper re-rendering
|
||||||
@@ -221,6 +230,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.rendering = false
|
m.rendering = false
|
||||||
m.clipboard = msg.clipboard
|
m.clipboard = msg.clipboard
|
||||||
m.loading = false
|
m.loading = false
|
||||||
|
m.messagePositions = msg.messagePositions
|
||||||
m.tail = m.viewport.AtBottom()
|
m.tail = m.viewport.AtBottom()
|
||||||
|
|
||||||
// Preserve scroll across reflow
|
// Preserve scroll across reflow
|
||||||
@@ -249,11 +259,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type renderCompleteMsg struct {
|
type renderCompleteMsg struct {
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
clipboard []string
|
clipboard []string
|
||||||
header string
|
header string
|
||||||
partCount int
|
partCount int
|
||||||
lineCount int
|
lineCount int
|
||||||
|
messagePositions map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesComponent) renderView() tea.Cmd {
|
func (m *messagesComponent) renderView() tea.Cmd {
|
||||||
@@ -279,6 +290,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
blocks := make([]string, 0)
|
blocks := make([]string, 0)
|
||||||
partCount := 0
|
partCount := 0
|
||||||
lineCount := 0
|
lineCount := 0
|
||||||
|
messagePositions := make(map[string]int) // Track message ID to line position
|
||||||
|
|
||||||
orphanedToolCalls := make([]opencode.ToolPart, 0)
|
orphanedToolCalls := make([]opencode.ToolPart, 0)
|
||||||
|
|
||||||
@@ -301,6 +313,9 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
|
|
||||||
switch casted := message.Info.(type) {
|
switch casted := message.Info.(type) {
|
||||||
case opencode.UserMessage:
|
case opencode.UserMessage:
|
||||||
|
// Track the position of this user message
|
||||||
|
messagePositions[casted.ID] = lineCount
|
||||||
|
|
||||||
if casted.ID == m.app.Session.Revert.MessageID {
|
if casted.ID == m.app.Session.Revert.MessageID {
|
||||||
reverted = true
|
reverted = true
|
||||||
revertedMessageCount = 1
|
revertedMessageCount = 1
|
||||||
@@ -368,10 +383,8 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
)
|
)
|
||||||
|
|
||||||
author := m.app.Config.Username
|
author := m.app.Config.Username
|
||||||
if casted.ID > lastAssistantMessage {
|
isQueued := casted.ID > lastAssistantMessage
|
||||||
author += " [queued]"
|
key := m.cache.GenerateKey(casted.ID, part.Text, width, files, author, isQueued)
|
||||||
}
|
|
||||||
key := m.cache.GenerateKey(casted.ID, part.Text, width, files, author)
|
|
||||||
content, cached = m.cache.Get(key)
|
content, cached = m.cache.Get(key)
|
||||||
if !cached {
|
if !cached {
|
||||||
content = renderText(
|
content = renderText(
|
||||||
@@ -383,15 +396,10 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
width,
|
width,
|
||||||
files,
|
files,
|
||||||
false,
|
false,
|
||||||
|
isQueued,
|
||||||
fileParts,
|
fileParts,
|
||||||
agentParts,
|
agentParts,
|
||||||
)
|
)
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
m.cache.Set(key, content)
|
m.cache.Set(key, content)
|
||||||
}
|
}
|
||||||
if content != "" {
|
if content != "" {
|
||||||
@@ -464,16 +472,11 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
width,
|
width,
|
||||||
"",
|
"",
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
[]opencode.FilePart{},
|
[]opencode.FilePart{},
|
||||||
[]opencode.AgentPart{},
|
[]opencode.AgentPart{},
|
||||||
toolCallParts...,
|
toolCallParts...,
|
||||||
)
|
)
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
m.cache.Set(key, content)
|
m.cache.Set(key, content)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -486,16 +489,11 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
width,
|
width,
|
||||||
"",
|
"",
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
[]opencode.FilePart{},
|
[]opencode.FilePart{},
|
||||||
[]opencode.AgentPart{},
|
[]opencode.AgentPart{},
|
||||||
toolCallParts...,
|
toolCallParts...,
|
||||||
)
|
)
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if content != "" {
|
if content != "" {
|
||||||
partCount++
|
partCount++
|
||||||
@@ -536,12 +534,6 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
permission,
|
permission,
|
||||||
width,
|
width,
|
||||||
)
|
)
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
m.cache.Set(key, content)
|
m.cache.Set(key, content)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -552,12 +544,6 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
permission,
|
permission,
|
||||||
width,
|
width,
|
||||||
)
|
)
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if content != "" {
|
if content != "" {
|
||||||
partCount++
|
partCount++
|
||||||
@@ -583,15 +569,10 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
width,
|
width,
|
||||||
"",
|
"",
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
[]opencode.FilePart{},
|
[]opencode.FilePart{},
|
||||||
[]opencode.AgentPart{},
|
[]opencode.AgentPart{},
|
||||||
)
|
)
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
partCount++
|
partCount++
|
||||||
lineCount += lipgloss.Height(content) + 1
|
lineCount += lipgloss.Height(content) + 1
|
||||||
blocks = append(blocks, content)
|
blocks = append(blocks, content)
|
||||||
@@ -622,15 +603,10 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
width,
|
width,
|
||||||
"",
|
"",
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
[]opencode.FilePart{},
|
[]opencode.FilePart{},
|
||||||
[]opencode.AgentPart{},
|
[]opencode.AgentPart{},
|
||||||
)
|
)
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
partCount++
|
partCount++
|
||||||
lineCount += lipgloss.Height(content) + 1
|
lineCount += lipgloss.Height(content) + 1
|
||||||
blocks = append(blocks, content)
|
blocks = append(blocks, content)
|
||||||
@@ -645,12 +621,6 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
width,
|
width,
|
||||||
WithBorderColor(t.Error()),
|
WithBorderColor(t.Error()),
|
||||||
)
|
)
|
||||||
error = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
error,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
blocks = append(blocks, error)
|
blocks = append(blocks, error)
|
||||||
lineCount += lipgloss.Height(error) + 1
|
lineCount += lipgloss.Height(error) + 1
|
||||||
}
|
}
|
||||||
@@ -742,12 +712,6 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
m.app.CurrentPermission,
|
m.app.CurrentPermission,
|
||||||
width,
|
width,
|
||||||
)
|
)
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
if content != "" {
|
if content != "" {
|
||||||
partCount++
|
partCount++
|
||||||
lineCount += lipgloss.Height(content) + 1
|
lineCount += lipgloss.Height(content) + 1
|
||||||
@@ -811,11 +775,12 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return renderCompleteMsg{
|
return renderCompleteMsg{
|
||||||
header: header,
|
header: header,
|
||||||
clipboard: clipboard,
|
clipboard: clipboard,
|
||||||
viewport: viewport,
|
viewport: viewport,
|
||||||
partCount: partCount,
|
partCount: partCount,
|
||||||
lineCount: lineCount,
|
lineCount: lineCount,
|
||||||
|
messagePositions: messagePositions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -828,8 +793,17 @@ func (m *messagesComponent) renderHeader() string {
|
|||||||
headerWidth := m.width
|
headerWidth := m.width
|
||||||
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
|
bgColor := t.Background()
|
||||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
borderColor := t.BackgroundElement()
|
||||||
|
|
||||||
|
isChildSession := m.app.Session.ParentID != ""
|
||||||
|
if isChildSession {
|
||||||
|
bgColor = t.BackgroundElement()
|
||||||
|
borderColor = t.Accent()
|
||||||
|
}
|
||||||
|
|
||||||
|
base := styles.NewStyle().Foreground(t.Text()).Background(bgColor).Render
|
||||||
|
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(bgColor).Render
|
||||||
|
|
||||||
sessionInfo := ""
|
sessionInfo := ""
|
||||||
tokens := float64(0)
|
tokens := float64(0)
|
||||||
@@ -861,20 +835,44 @@ func (m *messagesComponent) renderHeader() string {
|
|||||||
sessionInfoText := formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel)
|
sessionInfoText := formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel)
|
||||||
sessionInfo = styles.NewStyle().
|
sessionInfo = styles.NewStyle().
|
||||||
Foreground(t.TextMuted()).
|
Foreground(t.TextMuted()).
|
||||||
Background(t.Background()).
|
Background(bgColor).
|
||||||
Render(sessionInfoText)
|
Render(sessionInfoText)
|
||||||
|
|
||||||
shareEnabled := m.app.Config.Share != opencode.ConfigShareDisabled
|
shareEnabled := m.app.Config.Share != opencode.ConfigShareDisabled
|
||||||
|
|
||||||
|
navHint := ""
|
||||||
|
if isChildSession {
|
||||||
|
navHint = base(" "+m.app.Keybind(commands.SessionChildCycleReverseCommand)) + muted(" back")
|
||||||
|
}
|
||||||
|
|
||||||
headerTextWidth := headerWidth
|
headerTextWidth := headerWidth
|
||||||
if !shareEnabled {
|
if isChildSession {
|
||||||
// +1 is to ensure there is always at least one space between header and session info
|
headerTextWidth -= lipgloss.Width(navHint)
|
||||||
headerTextWidth -= len(sessionInfoText) + 1
|
} else if !shareEnabled {
|
||||||
|
headerTextWidth -= lipgloss.Width(sessionInfoText)
|
||||||
}
|
}
|
||||||
headerText := util.ToMarkdown(
|
headerText := util.ToMarkdown(
|
||||||
"# "+m.app.Session.Title,
|
"# "+m.app.Session.Title,
|
||||||
headerTextWidth,
|
headerTextWidth,
|
||||||
t.Background(),
|
bgColor,
|
||||||
)
|
)
|
||||||
|
if isChildSession {
|
||||||
|
headerText = layout.Render(
|
||||||
|
layout.FlexOptions{
|
||||||
|
Background: &bgColor,
|
||||||
|
Direction: layout.Row,
|
||||||
|
Justify: layout.JustifySpaceBetween,
|
||||||
|
Align: layout.AlignStretch,
|
||||||
|
Width: headerTextWidth,
|
||||||
|
},
|
||||||
|
layout.FlexItem{
|
||||||
|
View: headerText,
|
||||||
|
},
|
||||||
|
layout.FlexItem{
|
||||||
|
View: navHint,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var items []layout.FlexItem
|
var items []layout.FlexItem
|
||||||
if shareEnabled {
|
if shareEnabled {
|
||||||
@@ -887,10 +885,9 @@ func (m *messagesComponent) renderHeader() string {
|
|||||||
items = []layout.FlexItem{{View: headerText}, {View: sessionInfo}}
|
items = []layout.FlexItem{{View: headerText}, {View: sessionInfo}}
|
||||||
}
|
}
|
||||||
|
|
||||||
background := t.Background()
|
|
||||||
headerRow := layout.Render(
|
headerRow := layout.Render(
|
||||||
layout.FlexOptions{
|
layout.FlexOptions{
|
||||||
Background: &background,
|
Background: &bgColor,
|
||||||
Direction: layout.Row,
|
Direction: layout.Row,
|
||||||
Justify: layout.JustifySpaceBetween,
|
Justify: layout.JustifySpaceBetween,
|
||||||
Align: layout.AlignStretch,
|
Align: layout.AlignStretch,
|
||||||
@@ -906,22 +903,16 @@ func (m *messagesComponent) renderHeader() string {
|
|||||||
|
|
||||||
header := strings.Join(headerLines, "\n")
|
header := strings.Join(headerLines, "\n")
|
||||||
header = styles.NewStyle().
|
header = styles.NewStyle().
|
||||||
Background(t.Background()).
|
Background(bgColor).
|
||||||
Width(headerWidth).
|
Width(headerWidth).
|
||||||
PaddingLeft(2).
|
PaddingLeft(2).
|
||||||
PaddingRight(2).
|
PaddingRight(2).
|
||||||
BorderLeft(true).
|
BorderLeft(true).
|
||||||
BorderRight(true).
|
BorderRight(true).
|
||||||
BorderBackground(t.Background()).
|
BorderBackground(t.Background()).
|
||||||
BorderForeground(t.BackgroundElement()).
|
BorderForeground(borderColor).
|
||||||
BorderStyle(lipgloss.ThickBorder()).
|
BorderStyle(lipgloss.ThickBorder()).
|
||||||
Render(header)
|
Render(header)
|
||||||
header = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
header,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
|
|
||||||
return "\n" + header + "\n"
|
return "\n" + header + "\n"
|
||||||
}
|
}
|
||||||
@@ -966,7 +957,7 @@ func formatTokensAndCost(
|
|||||||
|
|
||||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
formattedCost := fmt.Sprintf("$%.2f", cost)
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"%s/%d%% (%s)",
|
" %s/%d%% (%s)",
|
||||||
formattedTokens,
|
formattedTokens,
|
||||||
int(percentage),
|
int(percentage),
|
||||||
formattedCost,
|
formattedCost,
|
||||||
@@ -975,20 +966,22 @@ func formatTokensAndCost(
|
|||||||
|
|
||||||
func (m *messagesComponent) View() string {
|
func (m *messagesComponent) View() string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
|
bgColor := t.Background()
|
||||||
|
|
||||||
if m.loading {
|
if m.loading {
|
||||||
return lipgloss.Place(
|
return lipgloss.Place(
|
||||||
m.width,
|
m.width,
|
||||||
m.height,
|
m.height,
|
||||||
lipgloss.Center,
|
lipgloss.Center,
|
||||||
lipgloss.Center,
|
lipgloss.Center,
|
||||||
styles.NewStyle().Background(t.Background()).Render(""),
|
styles.NewStyle().Background(bgColor).Render(""),
|
||||||
styles.WhitespaceStyle(t.Background()),
|
styles.WhitespaceStyle(bgColor),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewport := m.viewport.View()
|
viewport := m.viewport.View()
|
||||||
return styles.NewStyle().
|
return styles.NewStyle().
|
||||||
Background(t.Background()).
|
Background(bgColor).
|
||||||
Render(m.header + "\n" + viewport)
|
Render(m.header + "\n" + viewport)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1206,15 +1199,23 @@ func (m *messagesComponent) RedoLastMessage() (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *messagesComponent) ScrollToMessage(messageID string) (tea.Model, tea.Cmd) {
|
||||||
|
if m.messagePositions == nil {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if position, exists := m.messagePositions[messageID]; exists {
|
||||||
|
m.viewport.SetYOffset(position)
|
||||||
|
m.tail = false // Stop auto-scrolling to bottom when manually navigating
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewMessagesComponent(app *app.App) MessagesComponent {
|
func NewMessagesComponent(app *app.App) MessagesComponent {
|
||||||
vp := viewport.New()
|
vp := viewport.New()
|
||||||
vp.KeyMap = viewport.KeyMap{}
|
vp.KeyMap = viewport.KeyMap{}
|
||||||
|
|
||||||
if app.State.ScrollSpeed != nil && *app.State.ScrollSpeed > 0 {
|
vp.MouseWheelDelta = app.ScrollSpeed
|
||||||
vp.MouseWheelDelta = *app.State.ScrollSpeed
|
|
||||||
} else {
|
|
||||||
vp.MouseWheelDelta = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to showing tool details, hidden thinking blocks
|
// Default to showing tool details, hidden thinking blocks
|
||||||
showToolDetails := true
|
showToolDetails := true
|
||||||
@@ -1234,5 +1235,6 @@ func NewMessagesComponent(app *app.App) MessagesComponent {
|
|||||||
showThinkingBlocks: showThinkingBlocks,
|
showThinkingBlocks: showThinkingBlocks,
|
||||||
cache: NewPartCache(),
|
cache: NewPartCache(),
|
||||||
tail: true,
|
tail: true,
|
||||||
|
messagePositions: make(map[string]int),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ func (c *commandsComponent) View() string {
|
|||||||
}
|
}
|
||||||
commandsToShow = append(commandsToShow,
|
commandsToShow = append(commandsToShow,
|
||||||
// empty line
|
// empty line
|
||||||
commands.Command{
|
// commands.Command{
|
||||||
Name: "",
|
// Name: "",
|
||||||
Description: "",
|
// Description: "",
|
||||||
},
|
// },
|
||||||
commands.Command{
|
commands.Command{
|
||||||
Name: commands.CommandName(util.Ide()),
|
Name: commands.CommandName(util.Ide()),
|
||||||
Description: "open opencode",
|
Description: "open opencode",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package dialog
|
package dialog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"github.com/charmbracelet/bubbles/v2/key"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
@@ -19,9 +19,10 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
numVisibleAgents = 10
|
numVisibleAgents = 10
|
||||||
minAgentDialogWidth = 54
|
minAgentDialogWidth = 40
|
||||||
maxAgentDialogWidth = 108
|
maxAgentDialogWidth = 60
|
||||||
maxDescriptionLength = 80
|
maxDescriptionLength = 60
|
||||||
|
maxRecentAgents = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgentDialog interface for the agent selection dialog
|
// AgentDialog interface for the agent selection dialog
|
||||||
@@ -31,7 +32,7 @@ type AgentDialog interface {
|
|||||||
|
|
||||||
type agentDialog struct {
|
type agentDialog struct {
|
||||||
app *app.App
|
app *app.App
|
||||||
allAgents []opencode.Agent
|
allAgents []agentSelectItem
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
modal *modal.Modal
|
modal *modal.Modal
|
||||||
@@ -39,24 +40,31 @@ type agentDialog struct {
|
|||||||
dialogWidth int
|
dialogWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
// agentItem is a custom list item for agent selections
|
// agentSelectItem combines the visual improvements with code patterns
|
||||||
type agentItem struct {
|
type agentSelectItem struct {
|
||||||
agent opencode.Agent
|
name string
|
||||||
|
displayName string
|
||||||
|
description string
|
||||||
|
mode string // "primary", "subagent", "all"
|
||||||
|
isCurrent bool
|
||||||
|
agentIndex int
|
||||||
|
agent opencode.Agent // Keep original agent for compatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a agentItem) Render(
|
func (a agentSelectItem) Render(
|
||||||
selected bool,
|
selected bool,
|
||||||
width int,
|
width int,
|
||||||
baseStyle styles.Style,
|
baseStyle styles.Style,
|
||||||
) string {
|
) string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
|
|
||||||
itemStyle := baseStyle.
|
itemStyle := baseStyle.
|
||||||
Background(t.BackgroundPanel()).
|
Background(t.BackgroundPanel()).
|
||||||
Foreground(t.Text())
|
Foreground(t.Text())
|
||||||
|
|
||||||
if selected {
|
if selected {
|
||||||
itemStyle = itemStyle.Foreground(t.Primary())
|
// Use agent color for highlighting when selected (visual improvement)
|
||||||
|
agentColor := util.GetAgentColor(a.agentIndex)
|
||||||
|
itemStyle = itemStyle.Foreground(agentColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
descStyle := baseStyle.
|
descStyle := baseStyle.
|
||||||
@@ -66,25 +74,38 @@ func (a agentItem) Render(
|
|||||||
// Calculate available width (accounting for padding and margins)
|
// Calculate available width (accounting for padding and margins)
|
||||||
availableWidth := width - 2 // Account for left padding
|
availableWidth := width - 2 // Account for left padding
|
||||||
|
|
||||||
agentName := a.agent.Name
|
agentName := a.displayName
|
||||||
description := a.agent.Description
|
|
||||||
if description == "" {
|
// Determine if agent is built-in or custom using the agent's builtIn field
|
||||||
description = fmt.Sprintf("(%s)", a.agent.Mode)
|
var displayText string
|
||||||
|
if a.agent.BuiltIn {
|
||||||
|
displayText = "(built-in)"
|
||||||
|
} else {
|
||||||
|
if a.description != "" {
|
||||||
|
displayText = a.description
|
||||||
|
} else {
|
||||||
|
displayText = "(user)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
separator := " - "
|
separator := " - "
|
||||||
|
|
||||||
// Calculate how much space we have for the description
|
// Calculate how much space we have for the description (visual improvement)
|
||||||
nameAndSeparatorLength := len(agentName) + len(separator)
|
nameAndSeparatorLength := len(agentName) + len(separator)
|
||||||
descriptionMaxLength := availableWidth - nameAndSeparatorLength
|
descriptionMaxLength := availableWidth - nameAndSeparatorLength
|
||||||
|
|
||||||
// Truncate description if it's too long
|
// Cap description length to the maximum allowed
|
||||||
if len(description) > descriptionMaxLength && descriptionMaxLength > 3 {
|
if descriptionMaxLength > maxDescriptionLength {
|
||||||
description = description[:descriptionMaxLength-3] + "..."
|
descriptionMaxLength = maxDescriptionLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate description if it's too long (visual improvement)
|
||||||
|
if len(displayText) > descriptionMaxLength && descriptionMaxLength > 3 {
|
||||||
|
displayText = displayText[:descriptionMaxLength-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
namePart := itemStyle.Render(agentName)
|
namePart := itemStyle.Render(agentName)
|
||||||
descPart := descStyle.Render(separator + description)
|
descPart := descStyle.Render(separator + displayText)
|
||||||
combinedText := namePart + descPart
|
combinedText := namePart + descPart
|
||||||
|
|
||||||
return baseStyle.
|
return baseStyle.
|
||||||
@@ -94,8 +115,7 @@ func (a agentItem) Render(
|
|||||||
Render(combinedText)
|
Render(combinedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a agentItem) Selectable() bool {
|
func (a agentSelectItem) Selectable() bool {
|
||||||
// All agents in the dialog are selectable (subagents are filtered out)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,32 +142,43 @@ func (a *agentDialog) Init() tea.Cmd {
|
|||||||
|
|
||||||
func (a *agentDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (a *agentDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
a.width = msg.Width
|
||||||
|
a.height = msg.Height
|
||||||
|
a.searchDialog.SetWidth(a.dialogWidth)
|
||||||
|
a.searchDialog.SetHeight(msg.Height)
|
||||||
|
|
||||||
case SearchSelectionMsg:
|
case SearchSelectionMsg:
|
||||||
// Handle selection from search dialog
|
// Handle selection from search dialog
|
||||||
if item, ok := msg.Item.(agentItem); ok {
|
if item, ok := msg.Item.(agentSelectItem); ok {
|
||||||
return a, tea.Sequence(
|
if !item.isCurrent {
|
||||||
util.CmdHandler(modal.CloseModalMsg{}),
|
// Switch to selected agent (using their better pattern)
|
||||||
util.CmdHandler(
|
return a, tea.Sequence(
|
||||||
app.AgentSelectedMsg{
|
util.CmdHandler(modal.CloseModalMsg{}),
|
||||||
Agent: item.agent,
|
util.CmdHandler(app.AgentSelectedMsg{AgentName: item.name}),
|
||||||
}),
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
return a, util.CmdHandler(modal.CloseModalMsg{})
|
return a, util.CmdHandler(modal.CloseModalMsg{})
|
||||||
case SearchCancelledMsg:
|
case SearchCancelledMsg:
|
||||||
return a, util.CmdHandler(modal.CloseModalMsg{})
|
return a, util.CmdHandler(modal.CloseModalMsg{})
|
||||||
|
|
||||||
|
case SearchRemoveItemMsg:
|
||||||
|
if item, ok := msg.Item.(agentSelectItem); ok {
|
||||||
|
if a.isAgentInRecentSection(item, msg.Index) {
|
||||||
|
a.app.State.RemoveAgentFromRecentlyUsed(item.name)
|
||||||
|
items := a.buildDisplayList(a.searchDialog.GetQuery())
|
||||||
|
a.searchDialog.SetItems(items)
|
||||||
|
return a, a.app.SaveState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
|
||||||
case SearchQueryChangedMsg:
|
case SearchQueryChangedMsg:
|
||||||
// Update the list based on search query
|
// Update the list based on search query
|
||||||
items := a.buildDisplayList(msg.Query)
|
items := a.buildDisplayList(msg.Query)
|
||||||
a.searchDialog.SetItems(items)
|
a.searchDialog.SetItems(items)
|
||||||
return a, nil
|
return a, nil
|
||||||
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
a.width = msg.Width
|
|
||||||
a.height = msg.Height
|
|
||||||
a.searchDialog.SetWidth(a.dialogWidth)
|
|
||||||
a.searchDialog.SetHeight(msg.Height)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedDialog, cmd := a.searchDialog.Update(msg)
|
updatedDialog, cmd := a.searchDialog.Update(msg)
|
||||||
@@ -155,20 +186,34 @@ func (a *agentDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return a, cmd
|
return a, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *agentDialog) SetSize(width, height int) {
|
||||||
|
a.width = width
|
||||||
|
a.height = height
|
||||||
|
}
|
||||||
|
|
||||||
func (a *agentDialog) View() string {
|
func (a *agentDialog) View() string {
|
||||||
return a.searchDialog.View()
|
return a.searchDialog.View()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agentDialog) calculateOptimalWidth(agents []opencode.Agent) int {
|
func (a *agentDialog) calculateOptimalWidth(agents []agentSelectItem) int {
|
||||||
maxWidth := minAgentDialogWidth
|
maxWidth := minAgentDialogWidth
|
||||||
|
|
||||||
for _, agent := range agents {
|
for _, agent := range agents {
|
||||||
// Calculate the width needed for this item: "AgentName - Description"
|
// Calculate the width needed for this item: "AgentName - Description" (visual improvement)
|
||||||
itemWidth := len(agent.Name)
|
itemWidth := len(agent.displayName)
|
||||||
if agent.Description != "" {
|
|
||||||
itemWidth += len(agent.Description) + 3 // " - "
|
if agent.agent.BuiltIn {
|
||||||
|
itemWidth += len("(built-in)") + 3 // " - "
|
||||||
} else {
|
} else {
|
||||||
itemWidth += len(string(agent.Mode)) + 3 // " (mode)"
|
if agent.description != "" {
|
||||||
|
descLength := len(agent.description)
|
||||||
|
if descLength > maxDescriptionLength {
|
||||||
|
descLength = maxDescriptionLength
|
||||||
|
}
|
||||||
|
itemWidth += descLength + 3 // " - "
|
||||||
|
} else {
|
||||||
|
itemWidth += len("(user)") + 3 // " - "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if itemWidth > maxWidth {
|
if itemWidth > maxWidth {
|
||||||
@@ -177,22 +222,37 @@ func (a *agentDialog) calculateOptimalWidth(agents []opencode.Agent) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
maxWidth = min(maxWidth, maxAgentDialogWidth)
|
maxWidth = min(maxWidth, maxAgentDialogWidth)
|
||||||
|
|
||||||
return maxWidth
|
return maxWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agentDialog) setupAllAgents() {
|
func (a *agentDialog) setupAllAgents() {
|
||||||
// Get agents from the app, filtering out subagents
|
currentAgentName := a.app.Agent().Name
|
||||||
a.allAgents = []opencode.Agent{}
|
|
||||||
for _, agent := range a.app.Agents {
|
// Build agent items from app.Agents (no API call needed) - their pattern
|
||||||
if agent.Mode != "subagent" {
|
a.allAgents = make([]agentSelectItem, 0, len(a.app.Agents))
|
||||||
a.allAgents = append(a.allAgents, agent)
|
for i, agent := range a.app.Agents {
|
||||||
|
if agent.Mode == "subagent" {
|
||||||
|
continue // Skip subagents entirely
|
||||||
}
|
}
|
||||||
|
isCurrent := agent.Name == currentAgentName
|
||||||
|
|
||||||
|
// Create display name (capitalize first letter)
|
||||||
|
displayName := strings.Title(agent.Name)
|
||||||
|
|
||||||
|
a.allAgents = append(a.allAgents, agentSelectItem{
|
||||||
|
name: agent.Name,
|
||||||
|
displayName: displayName,
|
||||||
|
description: agent.Description, // Keep for search but don't use in display
|
||||||
|
mode: string(agent.Mode),
|
||||||
|
isCurrent: isCurrent,
|
||||||
|
agentIndex: i,
|
||||||
|
agent: agent, // Keep original for compatibility
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
a.sortAgents()
|
a.sortAgents()
|
||||||
|
|
||||||
// Calculate optimal width based on all agents
|
// Calculate optimal width based on all agents (visual improvement)
|
||||||
a.dialogWidth = a.calculateOptimalWidth(a.allAgents)
|
a.dialogWidth = a.calculateOptimalWidth(a.allAgents)
|
||||||
|
|
||||||
// Ensure minimum width to prevent textinput issues
|
// Ensure minimum width to prevent textinput issues
|
||||||
@@ -201,6 +261,7 @@ func (a *agentDialog) setupAllAgents() {
|
|||||||
a.searchDialog = NewSearchDialog("Search agents...", numVisibleAgents)
|
a.searchDialog = NewSearchDialog("Search agents...", numVisibleAgents)
|
||||||
a.searchDialog.SetWidth(a.dialogWidth)
|
a.searchDialog.SetWidth(a.dialogWidth)
|
||||||
|
|
||||||
|
// Build initial display list (empty query shows grouped view)
|
||||||
items := a.buildDisplayList("")
|
items := a.buildDisplayList("")
|
||||||
a.searchDialog.SetItems(items)
|
a.searchDialog.SetItems(items)
|
||||||
}
|
}
|
||||||
@@ -210,42 +271,43 @@ func (a *agentDialog) sortAgents() {
|
|||||||
agentA := a.allAgents[i]
|
agentA := a.allAgents[i]
|
||||||
agentB := a.allAgents[j]
|
agentB := a.allAgents[j]
|
||||||
|
|
||||||
// Current agent goes first
|
// Current agent goes first (your preference)
|
||||||
if agentA.Name == a.app.Agent().Name {
|
if agentA.name == a.app.Agent().Name {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if agentB.Name == a.app.Agent().Name {
|
if agentB.name == a.app.Agent().Name {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alphabetical order for all other agents
|
// Alphabetical order for all other agents
|
||||||
return agentA.Name < agentB.Name
|
return agentA.name < agentB.name
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildDisplayList creates the list items based on search query
|
||||||
func (a *agentDialog) buildDisplayList(query string) []list.Item {
|
func (a *agentDialog) buildDisplayList(query string) []list.Item {
|
||||||
if query != "" {
|
if query != "" {
|
||||||
|
// Search mode: use fuzzy matching
|
||||||
return a.buildSearchResults(query)
|
return a.buildSearchResults(query)
|
||||||
|
} else {
|
||||||
|
// Grouped mode: show Recent agents section and alphabetical list (their pattern)
|
||||||
|
return a.buildGroupedResults()
|
||||||
}
|
}
|
||||||
return a.buildGroupedResults()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildSearchResults creates a flat list of search results using fuzzy matching
|
||||||
func (a *agentDialog) buildSearchResults(query string) []list.Item {
|
func (a *agentDialog) buildSearchResults(query string) []list.Item {
|
||||||
agentNames := []string{}
|
agentNames := []string{}
|
||||||
agentMap := make(map[string]opencode.Agent)
|
agentMap := make(map[string]agentSelectItem)
|
||||||
|
|
||||||
for _, agent := range a.allAgents {
|
for _, agent := range a.allAgents {
|
||||||
// Search by name
|
// Only include non-subagents in search
|
||||||
searchStr := agent.Name
|
if agent.mode == "subagent" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
searchStr := agent.name
|
||||||
agentNames = append(agentNames, searchStr)
|
agentNames = append(agentNames, searchStr)
|
||||||
agentMap[searchStr] = agent
|
agentMap[searchStr] = agent
|
||||||
|
|
||||||
// Search by description if available
|
|
||||||
if agent.Description != "" {
|
|
||||||
searchStr = fmt.Sprintf("%s %s", agent.Name, agent.Description)
|
|
||||||
agentNames = append(agentNames, searchStr)
|
|
||||||
agentMap[searchStr] = agent
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
matches := fuzzy.RankFindFold(query, agentNames)
|
matches := fuzzy.RankFindFold(query, agentNames)
|
||||||
@@ -257,25 +319,55 @@ func (a *agentDialog) buildSearchResults(query string) []list.Item {
|
|||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
agent := agentMap[match.Target]
|
agent := agentMap[match.Target]
|
||||||
// Create a unique key to avoid duplicates
|
// Create a unique key to avoid duplicates
|
||||||
key := agent.Name
|
key := agent.name
|
||||||
if seenAgents[key] {
|
if seenAgents[key] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seenAgents[key] = true
|
seenAgents[key] = true
|
||||||
items = append(items, agentItem{agent: agent})
|
items = append(items, agent)
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildGroupedResults creates a grouped list with Recent agents section and categorized agents
|
||||||
func (a *agentDialog) buildGroupedResults() []list.Item {
|
func (a *agentDialog) buildGroupedResults() []list.Item {
|
||||||
var items []list.Item
|
var items []list.Item
|
||||||
|
|
||||||
items = append(items, list.HeaderItem("Agents"))
|
// Add Recent section (their pattern)
|
||||||
|
recentAgents := a.getRecentAgents(maxRecentAgents)
|
||||||
|
if len(recentAgents) > 0 {
|
||||||
|
items = append(items, list.HeaderItem("Recent"))
|
||||||
|
for _, agent := range recentAgents {
|
||||||
|
items = append(items, agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add all agents (subagents are already filtered out)
|
// Create map of recent agent names for filtering
|
||||||
|
recentAgentNames := make(map[string]bool)
|
||||||
|
for _, recent := range recentAgents {
|
||||||
|
recentAgentNames[recent.name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show non-subagents (primary/user) in the main section
|
||||||
|
mainAgents := make([]agentSelectItem, 0)
|
||||||
for _, agent := range a.allAgents {
|
for _, agent := range a.allAgents {
|
||||||
items = append(items, agentItem{agent: agent})
|
if !recentAgentNames[agent.name] {
|
||||||
|
mainAgents = append(mainAgents, agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort main agents alphabetically
|
||||||
|
sort.Slice(mainAgents, func(i, j int) bool {
|
||||||
|
return mainAgents[i].name < mainAgents[j].name
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add main agents section
|
||||||
|
if len(mainAgents) > 0 {
|
||||||
|
items = append(items, list.HeaderItem("Agents"))
|
||||||
|
for _, agent := range mainAgents {
|
||||||
|
items = append(items, agent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
@@ -285,10 +377,65 @@ func (a *agentDialog) Render(background string) string {
|
|||||||
return a.modal.Render(a.View(), background)
|
return a.modal.Render(a.View(), background)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *agentDialog) Close() tea.Cmd {
|
func (a *agentDialog) Close() tea.Cmd {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getRecentAgents returns the most recently used agents (their pattern)
|
||||||
|
func (a *agentDialog) getRecentAgents(limit int) []agentSelectItem {
|
||||||
|
var recentAgents []agentSelectItem
|
||||||
|
|
||||||
|
// Get recent agents from app state
|
||||||
|
for _, usage := range a.app.State.RecentlyUsedAgents {
|
||||||
|
if len(recentAgents) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the corresponding agent
|
||||||
|
for _, agent := range a.allAgents {
|
||||||
|
if agent.name == usage.AgentName {
|
||||||
|
recentAgents = append(recentAgents, agent)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no recent agents, use the current agent
|
||||||
|
if len(recentAgents) == 0 {
|
||||||
|
currentAgentName := a.app.Agent().Name
|
||||||
|
for _, agent := range a.allAgents {
|
||||||
|
if agent.name == currentAgentName {
|
||||||
|
recentAgents = append(recentAgents, agent)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recentAgents
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *agentDialog) isAgentInRecentSection(agent agentSelectItem, index int) bool {
|
||||||
|
// Only check if we're in grouped mode (no search query)
|
||||||
|
if a.searchDialog.GetQuery() != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
recentAgents := a.getRecentAgents(maxRecentAgents)
|
||||||
|
if len(recentAgents) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index 0 is the "Recent" header, so recent agents are at indices 1 to len(recentAgents)
|
||||||
|
if index >= 1 && index <= len(recentAgents) {
|
||||||
|
if index-1 < len(recentAgents) {
|
||||||
|
recentAgent := recentAgents[index-1]
|
||||||
|
return recentAgent.name == agent.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func NewAgentDialog(app *app.App) AgentDialog {
|
func NewAgentDialog(app *app.App) AgentDialog {
|
||||||
dialog := &agentDialog{
|
dialog := &agentDialog{
|
||||||
app: app,
|
app: app,
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
package dialog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
|
||||||
"github.com/sst/opencode/internal/completions"
|
|
||||||
"github.com/sst/opencode/internal/components/list"
|
|
||||||
"github.com/sst/opencode/internal/components/modal"
|
|
||||||
"github.com/sst/opencode/internal/layout"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
|
||||||
"github.com/sst/opencode/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
findDialogWidth = 76
|
|
||||||
)
|
|
||||||
|
|
||||||
type FindSelectedMsg struct {
|
|
||||||
FilePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
type FindDialogCloseMsg struct{}
|
|
||||||
|
|
||||||
type findInitialSuggestionsMsg struct {
|
|
||||||
suggestions []completions.CompletionSuggestion
|
|
||||||
}
|
|
||||||
|
|
||||||
type FindDialog interface {
|
|
||||||
layout.Modal
|
|
||||||
tea.Model
|
|
||||||
tea.ViewModel
|
|
||||||
SetWidth(width int)
|
|
||||||
SetHeight(height int)
|
|
||||||
IsEmpty() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// findItem is a custom list item for file suggestions
|
|
||||||
type findItem struct {
|
|
||||||
suggestion completions.CompletionSuggestion
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f findItem) Render(
|
|
||||||
selected bool,
|
|
||||||
width int,
|
|
||||||
baseStyle styles.Style,
|
|
||||||
) string {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
|
|
||||||
itemStyle := baseStyle.
|
|
||||||
Background(t.BackgroundPanel()).
|
|
||||||
Foreground(t.TextMuted())
|
|
||||||
|
|
||||||
if selected {
|
|
||||||
itemStyle = itemStyle.Foreground(t.Primary())
|
|
||||||
}
|
|
||||||
|
|
||||||
return itemStyle.PaddingLeft(1).Render(f.suggestion.Display(itemStyle))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f findItem) Selectable() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type findDialogComponent struct {
|
|
||||||
completionProvider completions.CompletionProvider
|
|
||||||
allSuggestions []completions.CompletionSuggestion
|
|
||||||
width, height int
|
|
||||||
modal *modal.Modal
|
|
||||||
searchDialog *SearchDialog
|
|
||||||
dialogWidth int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) Init() tea.Cmd {
|
|
||||||
return tea.Batch(
|
|
||||||
f.loadInitialSuggestions(),
|
|
||||||
f.searchDialog.Init(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) loadInitialSuggestions() tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
items, err := f.completionProvider.GetChildEntries("")
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to get initial completion items", "error", err)
|
|
||||||
return findInitialSuggestionsMsg{suggestions: []completions.CompletionSuggestion{}}
|
|
||||||
}
|
|
||||||
return findInitialSuggestionsMsg{suggestions: items}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case findInitialSuggestionsMsg:
|
|
||||||
// Handle initial suggestions setup
|
|
||||||
f.allSuggestions = msg.suggestions
|
|
||||||
|
|
||||||
// Calculate dialog width
|
|
||||||
f.dialogWidth = f.calculateDialogWidth()
|
|
||||||
|
|
||||||
// Initialize search dialog with calculated width
|
|
||||||
f.searchDialog = NewSearchDialog("Search files...", 10)
|
|
||||||
f.searchDialog.SetWidth(f.dialogWidth)
|
|
||||||
|
|
||||||
// Convert to list items
|
|
||||||
items := make([]list.Item, len(f.allSuggestions))
|
|
||||||
for i, suggestion := range f.allSuggestions {
|
|
||||||
items[i] = findItem{suggestion: suggestion}
|
|
||||||
}
|
|
||||||
f.searchDialog.SetItems(items)
|
|
||||||
|
|
||||||
// Update modal with calculated width
|
|
||||||
f.modal = modal.New(
|
|
||||||
modal.WithTitle("Find Files"),
|
|
||||||
modal.WithMaxWidth(f.dialogWidth+4),
|
|
||||||
)
|
|
||||||
|
|
||||||
return f, f.searchDialog.Init()
|
|
||||||
|
|
||||||
case []completions.CompletionSuggestion:
|
|
||||||
// Store suggestions and convert to findItem for the search dialog
|
|
||||||
f.allSuggestions = msg
|
|
||||||
items := make([]list.Item, len(msg))
|
|
||||||
for i, suggestion := range msg {
|
|
||||||
items[i] = findItem{suggestion: suggestion}
|
|
||||||
}
|
|
||||||
f.searchDialog.SetItems(items)
|
|
||||||
return f, nil
|
|
||||||
|
|
||||||
case SearchSelectionMsg:
|
|
||||||
// Handle selection from search dialog - now we can directly access the suggestion
|
|
||||||
if item, ok := msg.Item.(findItem); ok {
|
|
||||||
return f, f.selectFile(item.suggestion)
|
|
||||||
}
|
|
||||||
return f, nil
|
|
||||||
|
|
||||||
case SearchCancelledMsg:
|
|
||||||
return f, f.Close()
|
|
||||||
|
|
||||||
case SearchQueryChangedMsg:
|
|
||||||
// Update completion items based on search query
|
|
||||||
return f, func() tea.Msg {
|
|
||||||
items, err := f.completionProvider.GetChildEntries(msg.Query)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to get completion items", "error", err)
|
|
||||||
return []completions.CompletionSuggestion{}
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
f.width = msg.Width
|
|
||||||
f.height = msg.Height
|
|
||||||
// Recalculate width based on new viewport size
|
|
||||||
oldWidth := f.dialogWidth
|
|
||||||
f.dialogWidth = f.calculateDialogWidth()
|
|
||||||
if oldWidth != f.dialogWidth {
|
|
||||||
f.searchDialog.SetWidth(f.dialogWidth)
|
|
||||||
// Update modal max width too
|
|
||||||
f.modal = modal.New(
|
|
||||||
modal.WithTitle("Find Files"),
|
|
||||||
modal.WithMaxWidth(f.dialogWidth+4),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
f.searchDialog.SetHeight(msg.Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward all other messages to the search dialog
|
|
||||||
updatedDialog, cmd := f.searchDialog.Update(msg)
|
|
||||||
f.searchDialog = updatedDialog.(*SearchDialog)
|
|
||||||
return f, cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) View() string {
|
|
||||||
return f.searchDialog.View()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) calculateDialogWidth() int {
|
|
||||||
// Use fixed width unless viewport is smaller
|
|
||||||
if f.width > 0 && f.width < findDialogWidth+10 {
|
|
||||||
return f.width - 10
|
|
||||||
}
|
|
||||||
return findDialogWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) SetWidth(width int) {
|
|
||||||
f.width = width
|
|
||||||
f.searchDialog.SetWidth(f.dialogWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) SetHeight(height int) {
|
|
||||||
f.height = height
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) IsEmpty() bool {
|
|
||||||
return f.searchDialog.GetQuery() == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) selectFile(item completions.CompletionSuggestion) tea.Cmd {
|
|
||||||
return tea.Sequence(
|
|
||||||
f.Close(),
|
|
||||||
util.CmdHandler(FindSelectedMsg{
|
|
||||||
FilePath: item.Value,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) Render(background string) string {
|
|
||||||
return f.modal.Render(f.View(), background)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *findDialogComponent) Close() tea.Cmd {
|
|
||||||
f.searchDialog.SetQuery("")
|
|
||||||
f.searchDialog.Blur()
|
|
||||||
return util.CmdHandler(modal.CloseModalMsg{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFindDialog(completionProvider completions.CompletionProvider) FindDialog {
|
|
||||||
component := &findDialogComponent{
|
|
||||||
completionProvider: completionProvider,
|
|
||||||
dialogWidth: findDialogWidth,
|
|
||||||
allSuggestions: []completions.CompletionSuggestion{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create search dialog and modal with fixed width
|
|
||||||
component.searchDialog = NewSearchDialog("Search files...", 10)
|
|
||||||
component.searchDialog.SetWidth(findDialogWidth)
|
|
||||||
|
|
||||||
component.modal = modal.New(
|
|
||||||
modal.WithTitle("Find Files"),
|
|
||||||
modal.WithMaxWidth(findDialogWidth+4),
|
|
||||||
)
|
|
||||||
|
|
||||||
return component
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
package dialog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
|
||||||
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
|
||||||
"github.com/sst/opencode/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InitDialogCmp is a component that asks the user if they want to initialize the project.
|
|
||||||
type InitDialogCmp struct {
|
|
||||||
width, height int
|
|
||||||
selected int
|
|
||||||
keys initDialogKeyMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewInitDialogCmp creates a new InitDialogCmp.
|
|
||||||
func NewInitDialogCmp() InitDialogCmp {
|
|
||||||
return InitDialogCmp{
|
|
||||||
selected: 0,
|
|
||||||
keys: initDialogKeyMap{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type initDialogKeyMap struct {
|
|
||||||
Tab key.Binding
|
|
||||||
Left key.Binding
|
|
||||||
Right key.Binding
|
|
||||||
Enter key.Binding
|
|
||||||
Escape key.Binding
|
|
||||||
Y key.Binding
|
|
||||||
N key.Binding
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShortHelp implements key.Map.
|
|
||||||
func (k initDialogKeyMap) ShortHelp() []key.Binding {
|
|
||||||
return []key.Binding{
|
|
||||||
key.NewBinding(
|
|
||||||
key.WithKeys("tab", "left", "right"),
|
|
||||||
key.WithHelp("tab/←/→", "toggle selection"),
|
|
||||||
),
|
|
||||||
key.NewBinding(
|
|
||||||
key.WithKeys("enter"),
|
|
||||||
key.WithHelp("enter", "confirm"),
|
|
||||||
),
|
|
||||||
key.NewBinding(
|
|
||||||
key.WithKeys("esc", "q"),
|
|
||||||
key.WithHelp("esc/q", "cancel"),
|
|
||||||
),
|
|
||||||
key.NewBinding(
|
|
||||||
key.WithKeys("y", "n"),
|
|
||||||
key.WithHelp("y/n", "yes/no"),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FullHelp implements key.Map.
|
|
||||||
func (k initDialogKeyMap) FullHelp() [][]key.Binding {
|
|
||||||
return [][]key.Binding{k.ShortHelp()}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init implements tea.Model.
|
|
||||||
func (m InitDialogCmp) Init() tea.Cmd {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update implements tea.Model.
|
|
||||||
func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch {
|
|
||||||
case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
|
|
||||||
return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
|
|
||||||
case key.Matches(msg, key.NewBinding(key.WithKeys("tab", "left", "right", "h", "l"))):
|
|
||||||
m.selected = (m.selected + 1) % 2
|
|
||||||
return m, nil
|
|
||||||
case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
|
|
||||||
return m, util.CmdHandler(CloseInitDialogMsg{Initialize: m.selected == 0})
|
|
||||||
case key.Matches(msg, key.NewBinding(key.WithKeys("y"))):
|
|
||||||
return m, util.CmdHandler(CloseInitDialogMsg{Initialize: true})
|
|
||||||
case key.Matches(msg, key.NewBinding(key.WithKeys("n"))):
|
|
||||||
return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
|
|
||||||
}
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
m.width = msg.Width
|
|
||||||
m.height = msg.Height
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// View implements tea.Model.
|
|
||||||
func (m InitDialogCmp) View() string {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
|
||||||
|
|
||||||
// Calculate width needed for content
|
|
||||||
maxWidth := 60 // Width for explanation text
|
|
||||||
|
|
||||||
title := baseStyle.
|
|
||||||
Foreground(t.Primary()).
|
|
||||||
Bold(true).
|
|
||||||
Width(maxWidth).
|
|
||||||
Padding(0, 1).
|
|
||||||
Render("Initialize Project")
|
|
||||||
|
|
||||||
explanation := baseStyle.
|
|
||||||
Foreground(t.Text()).
|
|
||||||
Width(maxWidth).
|
|
||||||
Padding(0, 1).
|
|
||||||
Render("Initialization generates a new AGENTS.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
|
|
||||||
|
|
||||||
question := baseStyle.
|
|
||||||
Foreground(t.Text()).
|
|
||||||
Width(maxWidth).
|
|
||||||
Padding(1, 1).
|
|
||||||
Render("Would you like to initialize this project?")
|
|
||||||
|
|
||||||
maxWidth = min(maxWidth, m.width-10)
|
|
||||||
yesStyle := baseStyle
|
|
||||||
noStyle := baseStyle
|
|
||||||
|
|
||||||
if m.selected == 0 {
|
|
||||||
yesStyle = yesStyle.
|
|
||||||
Background(t.Primary()).
|
|
||||||
Foreground(t.Background()).
|
|
||||||
Bold(true)
|
|
||||||
noStyle = noStyle.
|
|
||||||
Background(t.Background()).
|
|
||||||
Foreground(t.Primary())
|
|
||||||
} else {
|
|
||||||
noStyle = noStyle.
|
|
||||||
Background(t.Primary()).
|
|
||||||
Foreground(t.Background()).
|
|
||||||
Bold(true)
|
|
||||||
yesStyle = yesStyle.
|
|
||||||
Background(t.Background()).
|
|
||||||
Foreground(t.Primary())
|
|
||||||
}
|
|
||||||
|
|
||||||
yes := yesStyle.Padding(0, 3).Render("Yes")
|
|
||||||
no := noStyle.Padding(0, 3).Render("No")
|
|
||||||
|
|
||||||
buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no)
|
|
||||||
buttons = baseStyle.
|
|
||||||
Width(maxWidth).
|
|
||||||
Padding(1, 0).
|
|
||||||
Render(buttons)
|
|
||||||
|
|
||||||
content := lipgloss.JoinVertical(
|
|
||||||
lipgloss.Left,
|
|
||||||
title,
|
|
||||||
baseStyle.Width(maxWidth).Render(""),
|
|
||||||
explanation,
|
|
||||||
question,
|
|
||||||
buttons,
|
|
||||||
baseStyle.Width(maxWidth).Render(""),
|
|
||||||
)
|
|
||||||
|
|
||||||
return baseStyle.Padding(1, 2).
|
|
||||||
Border(lipgloss.RoundedBorder()).
|
|
||||||
BorderBackground(t.Background()).
|
|
||||||
BorderForeground(t.TextMuted()).
|
|
||||||
Width(lipgloss.Width(content) + 4).
|
|
||||||
Render(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSize sets the size of the component.
|
|
||||||
func (m *InitDialogCmp) SetSize(width, height int) {
|
|
||||||
m.width = width
|
|
||||||
m.height = height
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
|
|
||||||
type CloseInitDialogMsg struct {
|
|
||||||
Initialize bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowInitDialogMsg is a message that is sent to show the init dialog.
|
|
||||||
type ShowInitDialogMsg struct {
|
|
||||||
Show bool
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
package dialog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
|
"github.com/muesli/reflow/truncate"
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/components/list"
|
||||||
|
"github.com/sst/opencode/internal/components/modal"
|
||||||
|
"github.com/sst/opencode/internal/layout"
|
||||||
|
"github.com/sst/opencode/internal/styles"
|
||||||
|
"github.com/sst/opencode/internal/theme"
|
||||||
|
"github.com/sst/opencode/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NavigationDialog interface for the session navigation dialog
|
||||||
|
type NavigationDialog interface {
|
||||||
|
layout.Modal
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollToMessageMsg is sent when a message should be scrolled to
|
||||||
|
type ScrollToMessageMsg struct {
|
||||||
|
MessageID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreToMessageMsg is sent when conversation should be restored to a specific message
|
||||||
|
type RestoreToMessageMsg struct {
|
||||||
|
MessageID string
|
||||||
|
Index int
|
||||||
|
}
|
||||||
|
|
||||||
|
// navigationItem represents a user message in the navigation list
|
||||||
|
type navigationItem struct {
|
||||||
|
messageID string
|
||||||
|
content string
|
||||||
|
timestamp time.Time
|
||||||
|
index int // Index in the full message list
|
||||||
|
toolCount int // Number of tools used in this message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n navigationItem) Render(
|
||||||
|
selected bool,
|
||||||
|
width int,
|
||||||
|
isFirstInViewport bool,
|
||||||
|
baseStyle styles.Style,
|
||||||
|
) string {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
infoStyle := baseStyle.Background(t.BackgroundPanel()).Foreground(t.Info()).Render
|
||||||
|
textStyle := baseStyle.Background(t.BackgroundPanel()).Foreground(t.Text()).Render
|
||||||
|
|
||||||
|
// Format timestamp - only apply color when not selected
|
||||||
|
var timeStr string
|
||||||
|
var timeVisualLen int
|
||||||
|
if selected {
|
||||||
|
timeStr = n.timestamp.Format("15:04") + " "
|
||||||
|
timeVisualLen = lipgloss.Width(timeStr)
|
||||||
|
} else {
|
||||||
|
timeStr = infoStyle(n.timestamp.Format("15:04") + " ")
|
||||||
|
timeVisualLen = lipgloss.Width(timeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool count display (fixed width for alignment) - only apply color when not selected
|
||||||
|
toolInfo := ""
|
||||||
|
toolInfoVisualLen := 0
|
||||||
|
if n.toolCount > 0 {
|
||||||
|
toolInfoText := fmt.Sprintf("(%d tools)", n.toolCount)
|
||||||
|
if selected {
|
||||||
|
toolInfo = toolInfoText
|
||||||
|
} else {
|
||||||
|
toolInfo = infoStyle(toolInfoText)
|
||||||
|
}
|
||||||
|
toolInfoVisualLen = lipgloss.Width(toolInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate available space for content
|
||||||
|
// Reserve space for: timestamp + space + toolInfo + padding + some buffer
|
||||||
|
reservedSpace := timeVisualLen + 1 + toolInfoVisualLen + 4
|
||||||
|
contentWidth := max(width-reservedSpace, 8)
|
||||||
|
|
||||||
|
truncatedContent := truncate.StringWithTail(
|
||||||
|
strings.Split(n.content, "\n")[0],
|
||||||
|
uint(contentWidth),
|
||||||
|
"...",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply normal text color to content for non-selected items
|
||||||
|
var styledContent string
|
||||||
|
if selected {
|
||||||
|
styledContent = truncatedContent
|
||||||
|
} else {
|
||||||
|
styledContent = textStyle(truncatedContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the line with proper spacing - content left-aligned, tools right-aligned
|
||||||
|
var text string
|
||||||
|
text = timeStr + styledContent
|
||||||
|
if toolInfo != "" {
|
||||||
|
bgColor := t.BackgroundPanel()
|
||||||
|
if selected {
|
||||||
|
bgColor = t.Primary()
|
||||||
|
}
|
||||||
|
text = layout.Render(
|
||||||
|
layout.FlexOptions{
|
||||||
|
Background: &bgColor,
|
||||||
|
Direction: layout.Row,
|
||||||
|
Justify: layout.JustifySpaceBetween,
|
||||||
|
Align: layout.AlignStretch,
|
||||||
|
Width: width - 2,
|
||||||
|
},
|
||||||
|
layout.FlexItem{
|
||||||
|
View: text,
|
||||||
|
},
|
||||||
|
layout.FlexItem{
|
||||||
|
View: toolInfo,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemStyle styles.Style
|
||||||
|
if selected {
|
||||||
|
itemStyle = baseStyle.
|
||||||
|
Background(t.Primary()).
|
||||||
|
Foreground(t.BackgroundElement()).
|
||||||
|
Width(width).
|
||||||
|
PaddingLeft(1)
|
||||||
|
} else {
|
||||||
|
itemStyle = baseStyle.PaddingLeft(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemStyle.Render(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n navigationItem) Selectable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type navigationDialog struct {
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
modal *modal.Modal
|
||||||
|
list list.List[navigationItem]
|
||||||
|
app *app.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigationDialog) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigationDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
n.width = msg.Width
|
||||||
|
n.height = msg.Height
|
||||||
|
n.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||||
|
case tea.KeyPressMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "up", "down":
|
||||||
|
// Handle navigation and immediately scroll to selected message
|
||||||
|
var cmd tea.Cmd
|
||||||
|
listModel, cmd := n.list.Update(msg)
|
||||||
|
n.list = listModel.(list.List[navigationItem])
|
||||||
|
|
||||||
|
// Get the newly selected item and scroll to it immediately
|
||||||
|
if item, idx := n.list.GetSelectedItem(); idx >= 0 {
|
||||||
|
return n, tea.Sequence(
|
||||||
|
cmd,
|
||||||
|
util.CmdHandler(ScrollToMessageMsg{MessageID: item.messageID}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return n, cmd
|
||||||
|
case "r":
|
||||||
|
// Restore conversation to selected message
|
||||||
|
if item, idx := n.list.GetSelectedItem(); idx >= 0 {
|
||||||
|
return n, tea.Sequence(
|
||||||
|
util.CmdHandler(RestoreToMessageMsg{MessageID: item.messageID, Index: item.index}),
|
||||||
|
util.CmdHandler(modal.CloseModalMsg{}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case "enter":
|
||||||
|
// Keep Enter functionality for closing the modal
|
||||||
|
if _, idx := n.list.GetSelectedItem(); idx >= 0 {
|
||||||
|
return n, util.CmdHandler(modal.CloseModalMsg{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd tea.Cmd
|
||||||
|
listModel, cmd := n.list.Update(msg)
|
||||||
|
n.list = listModel.(list.List[navigationItem])
|
||||||
|
return n, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigationDialog) Render(background string) string {
|
||||||
|
listView := n.list.View()
|
||||||
|
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
keyStyle := styles.NewStyle().
|
||||||
|
Foreground(t.Text()).
|
||||||
|
Background(t.BackgroundPanel()).
|
||||||
|
Bold(true).
|
||||||
|
Render
|
||||||
|
mutedStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).Render
|
||||||
|
|
||||||
|
helpText := keyStyle(
|
||||||
|
"↑/↓",
|
||||||
|
) + mutedStyle(
|
||||||
|
" jump ",
|
||||||
|
) + keyStyle(
|
||||||
|
"r",
|
||||||
|
) + mutedStyle(
|
||||||
|
" restore",
|
||||||
|
)
|
||||||
|
|
||||||
|
bgColor := t.BackgroundPanel()
|
||||||
|
helpView := styles.NewStyle().
|
||||||
|
Background(bgColor).
|
||||||
|
Width(layout.Current.Container.Width - 14).
|
||||||
|
PaddingLeft(1).
|
||||||
|
PaddingTop(1).
|
||||||
|
Render(helpText)
|
||||||
|
|
||||||
|
content := strings.Join([]string{listView, helpView}, "\n")
|
||||||
|
|
||||||
|
return n.modal.Render(content, background)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigationDialog) Close() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractMessagePreview extracts a preview from message parts
|
||||||
|
func extractMessagePreview(parts []opencode.PartUnion) string {
|
||||||
|
for _, part := range parts {
|
||||||
|
switch casted := part.(type) {
|
||||||
|
case opencode.TextPart:
|
||||||
|
text := strings.TrimSpace(casted.Text)
|
||||||
|
if text != "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "No text content"
|
||||||
|
}
|
||||||
|
|
||||||
|
// countToolsInResponse counts tools in the assistant's response to a user message
|
||||||
|
func countToolsInResponse(messages []app.Message, userMessageIndex int) int {
|
||||||
|
count := 0
|
||||||
|
// Look at subsequent messages to find the assistant's response
|
||||||
|
for i := userMessageIndex + 1; i < len(messages); i++ {
|
||||||
|
message := messages[i]
|
||||||
|
// If we hit another user message, stop looking
|
||||||
|
if _, isUser := message.Info.(opencode.UserMessage); isUser {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Count tools in this assistant message
|
||||||
|
for _, part := range message.Parts {
|
||||||
|
switch part.(type) {
|
||||||
|
case opencode.ToolPart:
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNavigationDialog creates a new session navigation dialog
|
||||||
|
func NewNavigationDialog(app *app.App) NavigationDialog {
|
||||||
|
var items []navigationItem
|
||||||
|
|
||||||
|
// Filter to only user messages and extract relevant info
|
||||||
|
for i, message := range app.Messages {
|
||||||
|
if userMsg, ok := message.Info.(opencode.UserMessage); ok {
|
||||||
|
preview := extractMessagePreview(message.Parts)
|
||||||
|
toolCount := countToolsInResponse(app.Messages, i)
|
||||||
|
|
||||||
|
items = append(items, navigationItem{
|
||||||
|
messageID: userMsg.ID,
|
||||||
|
content: preview,
|
||||||
|
timestamp: time.UnixMilli(int64(userMsg.Time.Created)),
|
||||||
|
index: i,
|
||||||
|
toolCount: toolCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listComponent := list.NewListComponent(
|
||||||
|
list.WithItems(items),
|
||||||
|
list.WithMaxVisibleHeight[navigationItem](12),
|
||||||
|
list.WithFallbackMessage[navigationItem]("No user messages in this session"),
|
||||||
|
list.WithAlphaNumericKeys[navigationItem](true),
|
||||||
|
list.WithRenderFunc(
|
||||||
|
func(item navigationItem, selected bool, width int, baseStyle styles.Style) string {
|
||||||
|
return item.Render(selected, width, false, baseStyle)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
list.WithSelectableFunc(func(item navigationItem) bool {
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
listComponent.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||||
|
|
||||||
|
return &navigationDialog{
|
||||||
|
list: listComponent,
|
||||||
|
app: app,
|
||||||
|
modal: modal.New(
|
||||||
|
modal.WithTitle("Jump to Message"),
|
||||||
|
modal.WithMaxWidth(layout.Current.Container.Width-8),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/v2/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/muesli/reflow/truncate"
|
"github.com/muesli/reflow/truncate"
|
||||||
"github.com/sst/opencode-sdk-go"
|
"github.com/sst/opencode-sdk-go"
|
||||||
@@ -110,6 +111,9 @@ type sessionDialog struct {
|
|||||||
list list.List[sessionItem]
|
list list.List[sessionItem]
|
||||||
app *app.App
|
app *app.App
|
||||||
deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index
|
deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index
|
||||||
|
renameMode bool
|
||||||
|
renameInput textinput.Model
|
||||||
|
renameIndex int // index of session being renamed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sessionDialog) Init() tea.Cmd {
|
func (s *sessionDialog) Init() tea.Cmd {
|
||||||
@@ -123,70 +127,136 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
s.height = msg.Height
|
s.height = msg.Height
|
||||||
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||||
case tea.KeyPressMsg:
|
case tea.KeyPressMsg:
|
||||||
switch msg.String() {
|
if s.renameMode {
|
||||||
case "enter":
|
switch msg.String() {
|
||||||
if s.deleteConfirmation >= 0 {
|
case "enter":
|
||||||
s.deleteConfirmation = -1
|
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) && idx == s.renameIndex {
|
||||||
|
newTitle := s.renameInput.Value()
|
||||||
|
if strings.TrimSpace(newTitle) != "" {
|
||||||
|
sessionToUpdate := s.sessions[idx]
|
||||||
|
return s, tea.Sequence(
|
||||||
|
func() tea.Msg {
|
||||||
|
ctx := context.Background()
|
||||||
|
err := s.app.UpdateSession(ctx, sessionToUpdate.ID, newTitle)
|
||||||
|
if err != nil {
|
||||||
|
return toast.NewErrorToast("Failed to rename session: " + err.Error())()
|
||||||
|
}
|
||||||
|
s.sessions[idx].Title = newTitle
|
||||||
|
s.renameMode = false
|
||||||
|
s.modal.SetTitle("Switch Session")
|
||||||
|
s.updateListItems()
|
||||||
|
return toast.NewSuccessToast("Session renamed successfully")()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.renameMode = false
|
||||||
|
s.modal.SetTitle("Switch Session")
|
||||||
s.updateListItems()
|
s.updateListItems()
|
||||||
return s, nil
|
return s, nil
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
s.renameInput, cmd = s.renameInput.Update(msg)
|
||||||
|
return s, cmd
|
||||||
}
|
}
|
||||||
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
} else {
|
||||||
selectedSession := s.sessions[idx]
|
switch msg.String() {
|
||||||
|
case "enter":
|
||||||
|
if s.deleteConfirmation >= 0 {
|
||||||
|
s.deleteConfirmation = -1
|
||||||
|
s.updateListItems()
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
||||||
|
selectedSession := s.sessions[idx]
|
||||||
|
return s, tea.Sequence(
|
||||||
|
util.CmdHandler(modal.CloseModalMsg{}),
|
||||||
|
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case "n":
|
||||||
return s, tea.Sequence(
|
return s, tea.Sequence(
|
||||||
util.CmdHandler(modal.CloseModalMsg{}),
|
util.CmdHandler(modal.CloseModalMsg{}),
|
||||||
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
|
util.CmdHandler(app.SessionClearedMsg{}),
|
||||||
)
|
)
|
||||||
}
|
case "r":
|
||||||
case "n":
|
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
||||||
return s, tea.Sequence(
|
s.renameMode = true
|
||||||
util.CmdHandler(modal.CloseModalMsg{}),
|
s.renameIndex = idx
|
||||||
util.CmdHandler(app.SessionClearedMsg{}),
|
s.setupRenameInput(s.sessions[idx].Title)
|
||||||
)
|
s.modal.SetTitle("Rename Session")
|
||||||
case "x", "delete", "backspace":
|
s.updateListItems()
|
||||||
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
return s, textinput.Blink
|
||||||
if s.deleteConfirmation == idx {
|
}
|
||||||
// Second press - actually delete the session
|
case "x", "delete", "backspace":
|
||||||
sessionToDelete := s.sessions[idx]
|
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
||||||
return s, tea.Sequence(
|
if s.deleteConfirmation == idx {
|
||||||
func() tea.Msg {
|
// Second press - actually delete the session
|
||||||
s.sessions = slices.Delete(s.sessions, idx, idx+1)
|
sessionToDelete := s.sessions[idx]
|
||||||
s.deleteConfirmation = -1
|
return s, tea.Sequence(
|
||||||
s.updateListItems()
|
func() tea.Msg {
|
||||||
return nil
|
s.sessions = slices.Delete(s.sessions, idx, idx+1)
|
||||||
},
|
s.deleteConfirmation = -1
|
||||||
s.deleteSession(sessionToDelete.ID),
|
s.updateListItems()
|
||||||
)
|
return nil
|
||||||
} else {
|
},
|
||||||
// First press - enter delete confirmation mode
|
s.deleteSession(sessionToDelete.ID),
|
||||||
s.deleteConfirmation = idx
|
)
|
||||||
|
} else {
|
||||||
|
// First press - enter delete confirmation mode
|
||||||
|
s.deleteConfirmation = idx
|
||||||
|
s.updateListItems()
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "esc":
|
||||||
|
if s.deleteConfirmation >= 0 {
|
||||||
|
s.deleteConfirmation = -1
|
||||||
s.updateListItems()
|
s.updateListItems()
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "esc":
|
|
||||||
if s.deleteConfirmation >= 0 {
|
|
||||||
s.deleteConfirmation = -1
|
|
||||||
s.updateListItems()
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd tea.Cmd
|
if !s.renameMode {
|
||||||
listModel, cmd := s.list.Update(msg)
|
var cmd tea.Cmd
|
||||||
s.list = listModel.(list.List[sessionItem])
|
listModel, cmd := s.list.Update(msg)
|
||||||
return s, cmd
|
s.list = listModel.(list.List[sessionItem])
|
||||||
|
return s, cmd
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sessionDialog) Render(background string) string {
|
func (s *sessionDialog) Render(background string) string {
|
||||||
|
if s.renameMode {
|
||||||
|
// Show rename input instead of list
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
renameView := s.renameInput.View()
|
||||||
|
|
||||||
|
mutedStyle := styles.NewStyle().
|
||||||
|
Foreground(t.TextMuted()).
|
||||||
|
Background(t.BackgroundPanel()).
|
||||||
|
Render
|
||||||
|
helpText := mutedStyle("Enter to confirm, Esc to cancel")
|
||||||
|
helpText = styles.NewStyle().PaddingLeft(1).PaddingTop(1).Render(helpText)
|
||||||
|
|
||||||
|
content := strings.Join([]string{renameView, helpText}, "\n")
|
||||||
|
return s.modal.Render(content, background)
|
||||||
|
}
|
||||||
|
|
||||||
listView := s.list.View()
|
listView := s.list.View()
|
||||||
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
keyStyle := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundPanel()).Render
|
keyStyle := styles.NewStyle().
|
||||||
|
Foreground(t.Text()).
|
||||||
|
Background(t.BackgroundPanel()).
|
||||||
|
Bold(true).
|
||||||
|
Render
|
||||||
mutedStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).Render
|
mutedStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).Render
|
||||||
|
|
||||||
leftHelp := keyStyle("n") + mutedStyle(" new session")
|
leftHelp := keyStyle("n") + mutedStyle(" new ") + keyStyle("r") + mutedStyle(" rename")
|
||||||
rightHelp := keyStyle("x/del") + mutedStyle(" delete session")
|
rightHelp := keyStyle("x/del") + mutedStyle(" delete")
|
||||||
|
|
||||||
bgColor := t.BackgroundPanel()
|
bgColor := t.BackgroundPanel()
|
||||||
helpText := layout.Render(layout.FlexOptions{
|
helpText := layout.Render(layout.FlexOptions{
|
||||||
@@ -203,6 +273,39 @@ func (s *sessionDialog) Render(background string) string {
|
|||||||
return s.modal.Render(content, background)
|
return s.modal.Render(content, background)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *sessionDialog) setupRenameInput(currentTitle string) {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
bgColor := t.BackgroundPanel()
|
||||||
|
textColor := t.Text()
|
||||||
|
textMutedColor := t.TextMuted()
|
||||||
|
|
||||||
|
s.renameInput = textinput.New()
|
||||||
|
s.renameInput.SetValue(currentTitle)
|
||||||
|
s.renameInput.Focus()
|
||||||
|
s.renameInput.CharLimit = 100
|
||||||
|
s.renameInput.SetWidth(layout.Current.Container.Width - 20)
|
||||||
|
|
||||||
|
s.renameInput.Styles.Blurred.Placeholder = styles.NewStyle().
|
||||||
|
Foreground(textMutedColor).
|
||||||
|
Background(bgColor).
|
||||||
|
Lipgloss()
|
||||||
|
s.renameInput.Styles.Blurred.Text = styles.NewStyle().
|
||||||
|
Foreground(textColor).
|
||||||
|
Background(bgColor).
|
||||||
|
Lipgloss()
|
||||||
|
s.renameInput.Styles.Focused.Placeholder = styles.NewStyle().
|
||||||
|
Foreground(textMutedColor).
|
||||||
|
Background(bgColor).
|
||||||
|
Lipgloss()
|
||||||
|
s.renameInput.Styles.Focused.Text = styles.NewStyle().
|
||||||
|
Foreground(textColor).
|
||||||
|
Background(bgColor).
|
||||||
|
Lipgloss()
|
||||||
|
s.renameInput.Styles.Focused.Prompt = styles.NewStyle().
|
||||||
|
Background(bgColor).
|
||||||
|
Lipgloss()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sessionDialog) updateListItems() {
|
func (s *sessionDialog) updateListItems() {
|
||||||
_, currentIdx := s.list.GetSelectedItem()
|
_, currentIdx := s.list.GetSelectedItem()
|
||||||
|
|
||||||
@@ -229,7 +332,22 @@ func (s *sessionDialog) deleteSession(sessionID string) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReopenSessionModalMsg is emitted when the session modal should be reopened
|
||||||
|
type ReopenSessionModalMsg struct{}
|
||||||
|
|
||||||
func (s *sessionDialog) Close() tea.Cmd {
|
func (s *sessionDialog) Close() tea.Cmd {
|
||||||
|
if s.renameMode {
|
||||||
|
// If in rename mode, exit rename mode and return a command to reopen the modal
|
||||||
|
s.renameMode = false
|
||||||
|
s.modal.SetTitle("Switch Session")
|
||||||
|
s.updateListItems()
|
||||||
|
|
||||||
|
// Return a command that will reopen the session modal
|
||||||
|
return func() tea.Msg {
|
||||||
|
return ReopenSessionModalMsg{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Normal close behavior
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +390,8 @@ func NewSessionDialog(app *app.App) SessionDialog {
|
|||||||
list: listComponent,
|
list: listComponent,
|
||||||
app: app,
|
app: app,
|
||||||
deleteConfirmation: -1,
|
deleteConfirmation: -1,
|
||||||
|
renameMode: false,
|
||||||
|
renameIndex: -1,
|
||||||
modal: modal.New(
|
modal: modal.New(
|
||||||
modal.WithTitle("Switch Session"),
|
modal.WithTitle("Switch Session"),
|
||||||
modal.WithMaxWidth(layout.Current.Container.Width-8),
|
modal.WithMaxWidth(layout.Current.Container.Width-8),
|
||||||
|
|||||||
@@ -1,281 +0,0 @@
|
|||||||
package fileviewer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
|
||||||
|
|
||||||
"github.com/sst/opencode/internal/app"
|
|
||||||
"github.com/sst/opencode/internal/commands"
|
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
|
||||||
"github.com/sst/opencode/internal/components/diff"
|
|
||||||
"github.com/sst/opencode/internal/layout"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
|
||||||
"github.com/sst/opencode/internal/util"
|
|
||||||
"github.com/sst/opencode/internal/viewport"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DiffStyle int
|
|
||||||
|
|
||||||
const (
|
|
||||||
DiffStyleSplit DiffStyle = iota
|
|
||||||
DiffStyleUnified
|
|
||||||
)
|
|
||||||
|
|
||||||
type Model struct {
|
|
||||||
app *app.App
|
|
||||||
width, height int
|
|
||||||
viewport viewport.Model
|
|
||||||
filename *string
|
|
||||||
content *string
|
|
||||||
isDiff *bool
|
|
||||||
diffStyle DiffStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
type fileRenderedMsg struct {
|
|
||||||
content string
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(app *app.App) Model {
|
|
||||||
vp := viewport.New()
|
|
||||||
m := Model{
|
|
||||||
app: app,
|
|
||||||
viewport: vp,
|
|
||||||
diffStyle: DiffStyleUnified,
|
|
||||||
}
|
|
||||||
if app.State.SplitDiff {
|
|
||||||
m.diffStyle = DiffStyleSplit
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
|
||||||
return m.viewport.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|
||||||
var cmds []tea.Cmd
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case fileRenderedMsg:
|
|
||||||
m.viewport.SetContent(msg.content)
|
|
||||||
return m, util.CmdHandler(app.FileRenderedMsg{
|
|
||||||
FilePath: *m.filename,
|
|
||||||
})
|
|
||||||
case dialog.ThemeSelectedMsg:
|
|
||||||
return m, m.render()
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch msg.String() {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vp, cmd := m.viewport.Update(msg)
|
|
||||||
m.viewport = vp
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) View() string {
|
|
||||||
if !m.HasFile() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
header := *m.filename
|
|
||||||
header = styles.NewStyle().
|
|
||||||
Padding(1, 2).
|
|
||||||
Width(m.width).
|
|
||||||
Background(theme.CurrentTheme().BackgroundElement()).
|
|
||||||
Foreground(theme.CurrentTheme().Text()).
|
|
||||||
Render(header)
|
|
||||||
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
|
|
||||||
close := m.app.Key(commands.FileCloseCommand)
|
|
||||||
diffToggle := m.app.Key(commands.FileDiffToggleCommand)
|
|
||||||
if m.isDiff == nil || *m.isDiff == false {
|
|
||||||
diffToggle = ""
|
|
||||||
}
|
|
||||||
layoutToggle := m.app.Key(commands.MessagesLayoutToggleCommand)
|
|
||||||
|
|
||||||
background := t.Background()
|
|
||||||
footer := layout.Render(
|
|
||||||
layout.FlexOptions{
|
|
||||||
Background: &background,
|
|
||||||
Direction: layout.Row,
|
|
||||||
Justify: layout.JustifyCenter,
|
|
||||||
Align: layout.AlignStretch,
|
|
||||||
Width: m.width - 2,
|
|
||||||
Gap: 5,
|
|
||||||
},
|
|
||||||
layout.FlexItem{
|
|
||||||
View: close,
|
|
||||||
},
|
|
||||||
layout.FlexItem{
|
|
||||||
View: layoutToggle,
|
|
||||||
},
|
|
||||||
layout.FlexItem{
|
|
||||||
View: diffToggle,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
footer = styles.NewStyle().Background(t.Background()).Padding(0, 1).Render(footer)
|
|
||||||
|
|
||||||
return header + "\n" + m.viewport.View() + "\n" + footer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) Clear() (Model, tea.Cmd) {
|
|
||||||
m.filename = nil
|
|
||||||
m.content = nil
|
|
||||||
m.isDiff = nil
|
|
||||||
return *m, m.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ToggleDiff() (Model, tea.Cmd) {
|
|
||||||
switch m.diffStyle {
|
|
||||||
case DiffStyleSplit:
|
|
||||||
m.diffStyle = DiffStyleUnified
|
|
||||||
default:
|
|
||||||
m.diffStyle = DiffStyleSplit
|
|
||||||
}
|
|
||||||
return *m, m.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) DiffStyle() DiffStyle {
|
|
||||||
return m.diffStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) HasFile() bool {
|
|
||||||
return m.filename != nil && m.content != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Filename() string {
|
|
||||||
if m.filename == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return *m.filename
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetSize(width, height int) (Model, tea.Cmd) {
|
|
||||||
if m.width != width || m.height != height {
|
|
||||||
m.width = width
|
|
||||||
m.height = height
|
|
||||||
m.viewport.SetWidth(width)
|
|
||||||
m.viewport.SetHeight(height - 4)
|
|
||||||
return *m, m.render()
|
|
||||||
}
|
|
||||||
return *m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetFile(filename string, content string, isDiff bool) (Model, tea.Cmd) {
|
|
||||||
m.filename = &filename
|
|
||||||
m.content = &content
|
|
||||||
m.isDiff = &isDiff
|
|
||||||
return *m, m.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) render() tea.Cmd {
|
|
||||||
if m.filename == nil || m.content == nil {
|
|
||||||
m.viewport.SetContent("")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return func() tea.Msg {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
var rendered string
|
|
||||||
|
|
||||||
if m.isDiff != nil && *m.isDiff {
|
|
||||||
diffResult := ""
|
|
||||||
var err error
|
|
||||||
if m.diffStyle == DiffStyleSplit {
|
|
||||||
diffResult, err = diff.FormatDiff(
|
|
||||||
*m.filename,
|
|
||||||
*m.content,
|
|
||||||
diff.WithWidth(m.width),
|
|
||||||
)
|
|
||||||
} else if m.diffStyle == DiffStyleUnified {
|
|
||||||
diffResult, err = diff.FormatUnifiedDiff(
|
|
||||||
*m.filename,
|
|
||||||
*m.content,
|
|
||||||
diff.WithWidth(m.width),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
rendered = styles.NewStyle().
|
|
||||||
Foreground(t.Error()).
|
|
||||||
Render(fmt.Sprintf("Error rendering diff: %v", err))
|
|
||||||
} else {
|
|
||||||
rendered = strings.TrimRight(diffResult, "\n")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rendered = util.RenderFile(
|
|
||||||
*m.filename,
|
|
||||||
*m.content,
|
|
||||||
m.width,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
rendered = styles.NewStyle().
|
|
||||||
Width(m.width).
|
|
||||||
Background(t.BackgroundPanel()).
|
|
||||||
Render(rendered)
|
|
||||||
|
|
||||||
return fileRenderedMsg{
|
|
||||||
content: rendered,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ScrollTo(line int) {
|
|
||||||
m.viewport.SetYOffset(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ScrollToBottom() {
|
|
||||||
m.viewport.GotoBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ScrollToTop() {
|
|
||||||
m.viewport.GotoTop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) PageUp() (Model, tea.Cmd) {
|
|
||||||
m.viewport.ViewUp()
|
|
||||||
return *m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) PageDown() (Model, tea.Cmd) {
|
|
||||||
m.viewport.ViewDown()
|
|
||||||
return *m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) HalfPageUp() (Model, tea.Cmd) {
|
|
||||||
m.viewport.HalfViewUp()
|
|
||||||
return *m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) HalfPageDown() (Model, tea.Cmd) {
|
|
||||||
m.viewport.HalfViewDown()
|
|
||||||
return *m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) AtTop() bool {
|
|
||||||
return m.viewport.AtTop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) AtBottom() bool {
|
|
||||||
return m.viewport.AtBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) ScrollPercent() float64 {
|
|
||||||
return m.viewport.ScrollPercent()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) TotalLineCount() int {
|
|
||||||
return m.viewport.TotalLineCount()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) VisibleLineCount() int {
|
|
||||||
return m.viewport.VisibleLineCount()
|
|
||||||
}
|
|
||||||
@@ -132,7 +132,7 @@ func (m *statusComponent) View() string {
|
|||||||
modeForeground = t.BackgroundPanel()
|
modeForeground = t.BackgroundPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
command := m.app.Commands[commands.SwitchAgentCommand]
|
command := m.app.Commands[commands.AgentCycleCommand]
|
||||||
kb := command.Keybindings[0]
|
kb := command.Keybindings[0]
|
||||||
key := kb.Key
|
key := kb.Key
|
||||||
if kb.RequiresLeader {
|
if kb.RequiresLeader {
|
||||||
|
|||||||
@@ -670,6 +670,28 @@ func (m *Model) InsertAttachment(att *attachment.Attachment) {
|
|||||||
m.SetCursorColumn(m.col)
|
m.SetCursorColumn(m.col)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeAttachmentAtCursor replaces the attachment at or immediately before the
|
||||||
|
// cursor with its textual display and positions the cursor at the end of the
|
||||||
|
// inserted text. Returns true if an attachment was removed.
|
||||||
|
func (m *Model) removeAttachmentAtCursor() bool {
|
||||||
|
att, startIdx, _ := m.isAttachmentAtCursor()
|
||||||
|
if att == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Replace the attachment element with the display runes
|
||||||
|
before := m.value[m.row][:startIdx]
|
||||||
|
after := m.value[m.row][startIdx+1:]
|
||||||
|
replacement := runesToInterfaces([]rune(att.Display))
|
||||||
|
newRow := make([]any, 0, len(before)+len(replacement)+len(after))
|
||||||
|
newRow = append(newRow, before...)
|
||||||
|
newRow = append(newRow, replacement...)
|
||||||
|
newRow = append(newRow, after...)
|
||||||
|
m.value[m.row] = newRow
|
||||||
|
m.col = startIdx + len(replacement)
|
||||||
|
m.SetCursorColumn(m.col)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ReplaceRange replaces text from startCol to endCol on the current row with the given string.
|
// ReplaceRange replaces text from startCol to endCol on the current row with the given string.
|
||||||
// This preserves attachments outside the replaced range.
|
// This preserves attachments outside the replaced range.
|
||||||
func (m *Model) ReplaceRange(startCol, endCol int, replacement string) {
|
func (m *Model) ReplaceRange(startCol, endCol int, replacement string) {
|
||||||
@@ -1577,6 +1599,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
m.deleteBeforeCursor()
|
m.deleteBeforeCursor()
|
||||||
case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
|
case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
|
||||||
|
// If the cursor is at or just after an attachment, convert it to text instead of deleting
|
||||||
|
if att, _, _ := m.isAttachmentAtCursor(); att != nil {
|
||||||
|
if m.removeAttachmentAtCursor() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
m.col = clamp(m.col, 0, len(m.value[m.row]))
|
m.col = clamp(m.col, 0, len(m.value[m.row]))
|
||||||
if m.col <= 0 {
|
if m.col <= 0 {
|
||||||
m.mergeLineAbove(m.row)
|
m.mergeLineAbove(m.row)
|
||||||
@@ -1587,6 +1615,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
m.SetCursorColumn(m.col - 1)
|
m.SetCursorColumn(m.col - 1)
|
||||||
}
|
}
|
||||||
case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
|
case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
|
||||||
|
// If the cursor is on an attachment, convert it to text instead of deleting
|
||||||
|
if att, _, _ := m.isAttachmentAtCursor(); att != nil {
|
||||||
|
if m.removeAttachmentAtCursor() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {
|
if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {
|
||||||
m.value[m.row] = slices.Delete(m.value[m.row], m.col, m.col+1)
|
m.value[m.row] = slices.Delete(m.value[m.row], m.col, m.col+1)
|
||||||
}
|
}
|
||||||
@@ -2045,6 +2079,109 @@ func itemWidth(item any) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// forceWrapAttachment splits an attachment's display text across multiple lines
|
||||||
|
func forceWrapAttachment(att *attachment.Attachment, width int) [][]any {
|
||||||
|
if width <= 0 {
|
||||||
|
return [][]any{{att}}
|
||||||
|
}
|
||||||
|
|
||||||
|
display := att.Display
|
||||||
|
displayRunes := []rune(display)
|
||||||
|
|
||||||
|
if len(displayRunes) <= width {
|
||||||
|
return [][]any{{att}}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines [][]any
|
||||||
|
start := 0
|
||||||
|
|
||||||
|
for start < len(displayRunes) {
|
||||||
|
// Calculate how many runes fit in this line
|
||||||
|
end := start + width
|
||||||
|
if end > len(displayRunes) {
|
||||||
|
end = len(displayRunes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a wrapped attachment for this segment
|
||||||
|
wrappedAtt := &attachment.Attachment{
|
||||||
|
ID: att.ID,
|
||||||
|
Type: att.Type,
|
||||||
|
Display: string(displayRunes[start:end]),
|
||||||
|
URL: att.URL,
|
||||||
|
Filename: att.Filename,
|
||||||
|
MediaType: att.MediaType,
|
||||||
|
Source: att.Source,
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, []any{wrappedAtt})
|
||||||
|
start = end
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// forceWrapWord splits a word that's too long to fit within the given width
|
||||||
|
func forceWrapWord(word []any, width int) [][]any {
|
||||||
|
if width <= 0 || len(word) == 0 {
|
||||||
|
return [][]any{word}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines [][]any
|
||||||
|
currentLine := []any{}
|
||||||
|
currentWidth := 0
|
||||||
|
|
||||||
|
for _, item := range word {
|
||||||
|
if att, ok := item.(*attachment.Attachment); ok {
|
||||||
|
// Handle attachment that might be too wide
|
||||||
|
attWidth := uniseg.StringWidth(att.Display)
|
||||||
|
|
||||||
|
// If the attachment display is too wide, split it
|
||||||
|
if attWidth > width {
|
||||||
|
// Finish current line if it has content
|
||||||
|
if len(currentLine) > 0 {
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
currentLine = []any{}
|
||||||
|
currentWidth = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the attachment display across multiple lines
|
||||||
|
wrappedAttachment := forceWrapAttachment(att, width)
|
||||||
|
lines = append(lines, wrappedAttachment...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If adding this attachment would exceed the width, start a new line
|
||||||
|
if currentWidth+attWidth > width && len(currentLine) > 0 {
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
currentLine = []any{}
|
||||||
|
currentWidth = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine = append(currentLine, item)
|
||||||
|
currentWidth += attWidth
|
||||||
|
} else if r, ok := item.(rune); ok {
|
||||||
|
itemWidth := rw.RuneWidth(r)
|
||||||
|
|
||||||
|
// If adding this rune would exceed the width, start a new line
|
||||||
|
if currentWidth+itemWidth > width && len(currentLine) > 0 {
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
currentLine = []any{}
|
||||||
|
currentWidth = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine = append(currentLine, item)
|
||||||
|
currentWidth += itemWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last line if it has content
|
||||||
|
if len(currentLine) > 0 {
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
func wrapInterfaces(content []any, width int) [][]any {
|
func wrapInterfaces(content []any, width int) [][]any {
|
||||||
if width <= 0 {
|
if width <= 0 {
|
||||||
return [][]any{content}
|
return [][]any{content}
|
||||||
@@ -2076,11 +2213,49 @@ func wrapInterfaces(content []any, width int) [][]any {
|
|||||||
if !inSpaces {
|
if !inSpaces {
|
||||||
// End of a word
|
// End of a word
|
||||||
if lineW > 0 && lineW+wordW > width {
|
if lineW > 0 && lineW+wordW > width {
|
||||||
lines = append(lines, word)
|
// If the word itself is too long to fit on a line, force-wrap it
|
||||||
lineW = wordW
|
if wordW > width {
|
||||||
|
wrappedLines := forceWrapWord(word, width)
|
||||||
|
lines = append(lines, wrappedLines...)
|
||||||
|
// Calculate width of the last wrapped line
|
||||||
|
lastLine := wrappedLines[len(wrappedLines)-1]
|
||||||
|
lineW = 0
|
||||||
|
for _, item := range lastLine {
|
||||||
|
if r, ok := item.(rune); ok {
|
||||||
|
lineW += rw.RuneWidth(r)
|
||||||
|
} else if att, ok := item.(*attachment.Attachment); ok {
|
||||||
|
lineW += uniseg.StringWidth(att.Display)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines = append(lines, word)
|
||||||
|
lineW = wordW
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
// Check if the word needs to be force-wrapped even when it fits on the current line
|
||||||
lineW += wordW
|
if wordW > width {
|
||||||
|
currentLine := lines[len(lines)-1]
|
||||||
|
wrappedWord := forceWrapWord(word, width-lineW)
|
||||||
|
if len(wrappedWord) > 0 {
|
||||||
|
lines[len(lines)-1] = append(currentLine, wrappedWord[0]...)
|
||||||
|
for i := 1; i < len(wrappedWord); i++ {
|
||||||
|
lines = append(lines, wrappedWord[i])
|
||||||
|
}
|
||||||
|
// Calculate width of the last wrapped line
|
||||||
|
lastLine := wrappedWord[len(wrappedWord)-1]
|
||||||
|
lineW = 0
|
||||||
|
for _, item := range lastLine {
|
||||||
|
if r, ok := item.(rune); ok {
|
||||||
|
lineW += rw.RuneWidth(r)
|
||||||
|
} else if att, ok := item.(*attachment.Attachment); ok {
|
||||||
|
lineW += uniseg.StringWidth(att.Display)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
||||||
|
lineW += wordW
|
||||||
|
}
|
||||||
}
|
}
|
||||||
word = nil
|
word = nil
|
||||||
wordW = 0
|
wordW = 0
|
||||||
@@ -2110,11 +2285,49 @@ func wrapInterfaces(content []any, width int) [][]any {
|
|||||||
// Handle any remaining word/spaces at the end of the content.
|
// Handle any remaining word/spaces at the end of the content.
|
||||||
if wordW > 0 {
|
if wordW > 0 {
|
||||||
if lineW > 0 && lineW+wordW > width {
|
if lineW > 0 && lineW+wordW > width {
|
||||||
lines = append(lines, word)
|
// If the word itself is too long to fit on a line, force-wrap it
|
||||||
lineW = wordW
|
if wordW > width {
|
||||||
|
wrappedLines := forceWrapWord(word, width)
|
||||||
|
lines = append(lines, wrappedLines...)
|
||||||
|
// Calculate width of the last wrapped line
|
||||||
|
lastLine := wrappedLines[len(wrappedLines)-1]
|
||||||
|
lineW = 0
|
||||||
|
for _, item := range lastLine {
|
||||||
|
if r, ok := item.(rune); ok {
|
||||||
|
lineW += rw.RuneWidth(r)
|
||||||
|
} else if att, ok := item.(*attachment.Attachment); ok {
|
||||||
|
lineW += uniseg.StringWidth(att.Display)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines = append(lines, word)
|
||||||
|
lineW = wordW
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
// Check if the word needs to be force-wrapped even when it fits on the current line
|
||||||
lineW += wordW
|
if wordW > width {
|
||||||
|
currentLine := lines[len(lines)-1]
|
||||||
|
wrappedWord := forceWrapWord(word, width-lineW)
|
||||||
|
if len(wrappedWord) > 0 {
|
||||||
|
lines[len(lines)-1] = append(currentLine, wrappedWord[0]...)
|
||||||
|
for i := 1; i < len(wrappedWord); i++ {
|
||||||
|
lines = append(lines, wrappedWord[i])
|
||||||
|
}
|
||||||
|
// Calculate width of the last wrapped line
|
||||||
|
lastLine := wrappedWord[len(wrappedWord)-1]
|
||||||
|
lineW = 0
|
||||||
|
for _, item := range lastLine {
|
||||||
|
if r, ok := item.(rune); ok {
|
||||||
|
lineW += rw.RuneWidth(r)
|
||||||
|
} else if att, ok := item.(*attachment.Attachment); ok {
|
||||||
|
lineW += uniseg.StringWidth(att.Display)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
||||||
|
lineW += wordW
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if spaceW > 0 {
|
if spaceW > 0 {
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package textarea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sst/opencode/internal/attachment"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemoveAttachmentAtCursor_ConvertsToText_WhenCursorAfterAttachment(t *testing.T) {
|
||||||
|
m := New()
|
||||||
|
m.InsertString("a ")
|
||||||
|
att := &attachment.Attachment{ID: "1", Display: "@file.txt"}
|
||||||
|
m.InsertAttachment(att)
|
||||||
|
m.InsertString(" b")
|
||||||
|
|
||||||
|
// Position cursor immediately after the attachment (index 3: 'a',' ',att,' ', 'b')
|
||||||
|
m.SetCursorColumn(3)
|
||||||
|
|
||||||
|
if ok := m.removeAttachmentAtCursor(); !ok {
|
||||||
|
t.Fatalf("expected removal to occur")
|
||||||
|
}
|
||||||
|
got := m.Value()
|
||||||
|
want := "a @file.txt b"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("expected %q, got %q", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAttachmentAtCursor_ConvertsToText_WhenCursorOnAttachment(t *testing.T) {
|
||||||
|
m := New()
|
||||||
|
m.InsertString("x ")
|
||||||
|
att := &attachment.Attachment{ID: "2", Display: "@img.png"}
|
||||||
|
m.InsertAttachment(att)
|
||||||
|
m.InsertString(" y")
|
||||||
|
|
||||||
|
// Position cursor on the attachment token (index 2: 'x',' ',att,' ', 'y')
|
||||||
|
m.SetCursorColumn(2)
|
||||||
|
|
||||||
|
if ok := m.removeAttachmentAtCursor(); !ok {
|
||||||
|
t.Fatalf("expected removal to occur")
|
||||||
|
}
|
||||||
|
got := m.Value()
|
||||||
|
want := "x @img.png y"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("expected %q, got %q", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAttachmentAtCursor_StartOfLine(t *testing.T) {
|
||||||
|
m := New()
|
||||||
|
att := &attachment.Attachment{ID: "3", Display: "@a.txt"}
|
||||||
|
m.InsertAttachment(att)
|
||||||
|
m.InsertString(" tail")
|
||||||
|
|
||||||
|
// Position cursor immediately after the attachment at start of line (index 1)
|
||||||
|
m.SetCursorColumn(1)
|
||||||
|
if ok := m.removeAttachmentAtCursor(); !ok {
|
||||||
|
t.Fatalf("expected removal to occur at start of line")
|
||||||
|
}
|
||||||
|
if got := m.Value(); got != "@a.txt tail" {
|
||||||
|
t.Fatalf("unexpected value: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAttachmentAtCursor_NoAttachment_NoChange(t *testing.T) {
|
||||||
|
m := New()
|
||||||
|
m.InsertString("hello world")
|
||||||
|
col := m.CursorColumn()
|
||||||
|
if ok := m.removeAttachmentAtCursor(); ok {
|
||||||
|
t.Fatalf("did not expect removal to occur")
|
||||||
|
}
|
||||||
|
if m.Value() != "hello world" || m.CursorColumn() != col {
|
||||||
|
t.Fatalf("value or cursor unexpectedly changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
+320
-139
@@ -23,7 +23,6 @@ import (
|
|||||||
"github.com/sst/opencode/internal/components/chat"
|
"github.com/sst/opencode/internal/components/chat"
|
||||||
cmdcomp "github.com/sst/opencode/internal/components/commands"
|
cmdcomp "github.com/sst/opencode/internal/components/commands"
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
"github.com/sst/opencode/internal/components/fileviewer"
|
|
||||||
"github.com/sst/opencode/internal/components/modal"
|
"github.com/sst/opencode/internal/components/modal"
|
||||||
"github.com/sst/opencode/internal/components/status"
|
"github.com/sst/opencode/internal/components/status"
|
||||||
"github.com/sst/opencode/internal/components/toast"
|
"github.com/sst/opencode/internal/components/toast"
|
||||||
@@ -59,6 +58,8 @@ const interruptDebounceTimeout = 1 * time.Second
|
|||||||
const exitDebounceTimeout = 1 * time.Second
|
const exitDebounceTimeout = 1 * time.Second
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
|
tea.Model
|
||||||
|
tea.CursorModel
|
||||||
width, height int
|
width, height int
|
||||||
app *app.App
|
app *app.App
|
||||||
modal layout.Modal
|
modal layout.Modal
|
||||||
@@ -76,7 +77,6 @@ type Model struct {
|
|||||||
interruptKeyState InterruptKeyState
|
interruptKeyState InterruptKeyState
|
||||||
exitKeyState ExitKeyState
|
exitKeyState ExitKeyState
|
||||||
messagesRight bool
|
messagesRight bool
|
||||||
fileViewer fileviewer.Model
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Model) Init() tea.Cmd {
|
func (a Model) Init() tea.Cmd {
|
||||||
@@ -92,13 +92,6 @@ func (a Model) Init() tea.Cmd {
|
|||||||
cmds = append(cmds, a.status.Init())
|
cmds = append(cmds, a.status.Init())
|
||||||
cmds = append(cmds, a.completions.Init())
|
cmds = append(cmds, a.completions.Init())
|
||||||
cmds = append(cmds, a.toastManager.Init())
|
cmds = append(cmds, a.toastManager.Init())
|
||||||
cmds = append(cmds, a.fileViewer.Init())
|
|
||||||
|
|
||||||
// Check if we should show the init dialog
|
|
||||||
cmds = append(cmds, func() tea.Msg {
|
|
||||||
shouldShow := a.app.Info.Git && a.app.Info.Time.Initialized > 0
|
|
||||||
return dialog.ShowInitDialogMsg{Show: shouldShow}
|
|
||||||
})
|
|
||||||
|
|
||||||
return tea.Batch(cmds...)
|
return tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
@@ -149,6 +142,23 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.app.IsBashMode {
|
||||||
|
if keyString == "backspace" && a.editor.Length() == 0 {
|
||||||
|
a.app.IsBashMode = false
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyString == "enter" || keyString == "esc" || keyString == "ctrl+c" {
|
||||||
|
a.app.IsBashMode = false
|
||||||
|
if keyString == "enter" {
|
||||||
|
updated, cmd := a.editor.SubmitBash()
|
||||||
|
a.editor = updated.(chat.EditorComponent)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
return a, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Handle active modal
|
// 1. Handle active modal
|
||||||
if a.modal != nil {
|
if a.modal != nil {
|
||||||
switch keyString {
|
switch keyString {
|
||||||
@@ -187,7 +197,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
// 3. Handle completions trigger
|
// 3. Handle completions trigger
|
||||||
if keyString == "/" &&
|
if keyString == "/" &&
|
||||||
!a.showCompletionDialog &&
|
!a.showCompletionDialog &&
|
||||||
a.editor.Value() == "" {
|
a.editor.Value() == "" &&
|
||||||
|
!a.app.IsBashMode {
|
||||||
a.showCompletionDialog = true
|
a.showCompletionDialog = true
|
||||||
|
|
||||||
updated, cmd := a.editor.Update(msg)
|
updated, cmd := a.editor.Update(msg)
|
||||||
@@ -205,7 +216,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
// Handle file completions trigger
|
// Handle file completions trigger
|
||||||
if keyString == "@" &&
|
if keyString == "@" &&
|
||||||
!a.showCompletionDialog {
|
!a.showCompletionDialog &&
|
||||||
|
!a.app.IsBashMode {
|
||||||
a.showCompletionDialog = true
|
a.showCompletionDialog = true
|
||||||
|
|
||||||
updated, cmd := a.editor.Update(msg)
|
updated, cmd := a.editor.Update(msg)
|
||||||
@@ -221,6 +233,11 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return a, tea.Sequence(cmds...)
|
return a, tea.Sequence(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if keyString == "!" && a.editor.Value() == "" {
|
||||||
|
a.app.IsBashMode = true
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
if a.showCompletionDialog {
|
if a.showCompletionDialog {
|
||||||
switch keyString {
|
switch keyString {
|
||||||
case "tab", "enter", "esc", "ctrl+c", "up", "down", "ctrl+p", "ctrl+n":
|
case "tab", "enter", "esc", "ctrl+c", "up", "down", "ctrl+p", "ctrl+n":
|
||||||
@@ -355,6 +372,11 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
a.modal = nil
|
a.modal = nil
|
||||||
return a, cmd
|
return a, cmd
|
||||||
|
case dialog.ReopenSessionModalMsg:
|
||||||
|
// Reopen the session modal (used when exiting rename mode)
|
||||||
|
sessionDialog := dialog.NewSessionDialog(a.app)
|
||||||
|
a.modal = sessionDialog
|
||||||
|
return a, nil
|
||||||
case commands.ExecuteCommandMsg:
|
case commands.ExecuteCommandMsg:
|
||||||
updated, cmd := a.executeCommand(commands.Command(msg))
|
updated, cmd := a.executeCommand(commands.Command(msg))
|
||||||
return updated, cmd
|
return updated, cmd
|
||||||
@@ -369,8 +391,41 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return a, toast.NewErrorToast(msg.Error())
|
return a, toast.NewErrorToast(msg.Error())
|
||||||
case app.SendPrompt:
|
case app.SendPrompt:
|
||||||
a.showCompletionDialog = false
|
a.showCompletionDialog = false
|
||||||
a.app, cmd = a.app.SendPrompt(context.Background(), msg)
|
// If we're in a child session, switch back to parent before sending prompt
|
||||||
cmds = append(cmds, cmd)
|
if a.app.Session.ParentID != "" {
|
||||||
|
parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get parent session", "error", err)
|
||||||
|
return a, toast.NewErrorToast("Failed to get parent session")
|
||||||
|
}
|
||||||
|
a.app.Session = parentSession
|
||||||
|
a.app, cmd = a.app.SendPrompt(context.Background(), msg)
|
||||||
|
cmds = append(cmds, tea.Sequence(
|
||||||
|
util.CmdHandler(app.SessionSelectedMsg(parentSession)),
|
||||||
|
cmd,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
a.app, cmd = a.app.SendPrompt(context.Background(), msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
case app.SendShell:
|
||||||
|
// If we're in a child session, switch back to parent before sending prompt
|
||||||
|
if a.app.Session.ParentID != "" {
|
||||||
|
parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get parent session", "error", err)
|
||||||
|
return a, toast.NewErrorToast("Failed to get parent session")
|
||||||
|
}
|
||||||
|
a.app.Session = parentSession
|
||||||
|
a.app, cmd = a.app.SendShell(context.Background(), msg.Command)
|
||||||
|
cmds = append(cmds, tea.Sequence(
|
||||||
|
util.CmdHandler(app.SessionSelectedMsg(parentSession)),
|
||||||
|
cmd,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
a.app, cmd = a.app.SendShell(context.Background(), msg.Command)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
case app.SetEditorContentMsg:
|
case app.SetEditorContentMsg:
|
||||||
// Set the editor content without sending
|
// Set the editor content without sending
|
||||||
a.editor.SetValueWithAttachments(msg.Text)
|
a.editor.SetValueWithAttachments(msg.Text)
|
||||||
@@ -552,12 +607,6 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
|
slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
|
||||||
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
|
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
|
||||||
}
|
}
|
||||||
case opencode.EventListResponseEventFileWatcherUpdated:
|
|
||||||
if a.fileViewer.HasFile() {
|
|
||||||
if a.fileViewer.Filename() == msg.Properties.File {
|
|
||||||
return a.openFile(msg.Properties.File)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
msg.Height -= 2 // Make space for the status bar
|
msg.Height -= 2 // Make space for the status bar
|
||||||
a.width, a.height = msg.Width, msg.Height
|
a.width, a.height = msg.Width, msg.Height
|
||||||
@@ -582,7 +631,39 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return a, util.CmdHandler(app.SessionLoadedMsg{})
|
return a, util.CmdHandler(app.SessionLoadedMsg{})
|
||||||
case app.SessionCreatedMsg:
|
case app.SessionCreatedMsg:
|
||||||
a.app.Session = msg.Session
|
a.app.Session = msg.Session
|
||||||
return a, util.CmdHandler(app.SessionLoadedMsg{})
|
case dialog.ScrollToMessageMsg:
|
||||||
|
updated, cmd := a.messages.ScrollToMessage(msg.MessageID)
|
||||||
|
a.messages = updated.(chat.MessagesComponent)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
case dialog.RestoreToMessageMsg:
|
||||||
|
cmd := func() tea.Msg {
|
||||||
|
// Find next user message after target
|
||||||
|
var nextMessageID string
|
||||||
|
for i := msg.Index + 1; i < len(a.app.Messages); i++ {
|
||||||
|
if userMsg, ok := a.app.Messages[i].Info.(opencode.UserMessage); ok {
|
||||||
|
nextMessageID = userMsg.ID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var response *opencode.Session
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if nextMessageID == "" {
|
||||||
|
// Last message - use unrevert to restore full conversation
|
||||||
|
response, err = a.app.Client.Session.Unrevert(context.Background(), a.app.Session.ID)
|
||||||
|
} else {
|
||||||
|
// Revert to next message to make target the last visible
|
||||||
|
response, err = a.app.Client.Session.Revert(context.Background(), a.app.Session.ID,
|
||||||
|
opencode.SessionRevertParams{MessageID: opencode.F(nextMessageID)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil || response == nil {
|
||||||
|
return toast.NewErrorToast("Failed to restore to message")
|
||||||
|
}
|
||||||
|
return app.MessageRevertedMsg{Session: *response, Message: app.Message{}}
|
||||||
|
}
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
case app.MessageRevertedMsg:
|
case app.MessageRevertedMsg:
|
||||||
if msg.Session.ID == a.app.Session.ID {
|
if msg.Session.ID == a.app.Session.ID {
|
||||||
a.app.Session = &msg.Session
|
a.app.Session = &msg.Session
|
||||||
@@ -597,31 +678,9 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
a.app.State.UpdateModelUsage(msg.Provider.ID, msg.Model.ID)
|
a.app.State.UpdateModelUsage(msg.Provider.ID, msg.Model.ID)
|
||||||
cmds = append(cmds, a.app.SaveState())
|
cmds = append(cmds, a.app.SaveState())
|
||||||
case app.AgentSelectedMsg:
|
case app.AgentSelectedMsg:
|
||||||
// Find the agent index
|
updated, cmd := a.app.SwitchToAgent(msg.AgentName)
|
||||||
for i, agent := range a.app.Agents {
|
a.app = updated
|
||||||
if agent.Name == msg.Agent.Name {
|
cmds = append(cmds, cmd)
|
||||||
a.app.AgentIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a.app.State.Agent = msg.Agent.Name
|
|
||||||
|
|
||||||
// Switch to the agent's preferred model if available
|
|
||||||
if model, ok := a.app.State.AgentModel[msg.Agent.Name]; ok {
|
|
||||||
for _, provider := range a.app.Providers {
|
|
||||||
if provider.ID == model.ProviderID {
|
|
||||||
a.app.Provider = &provider
|
|
||||||
for _, m := range provider.Models {
|
|
||||||
if m.ID == model.ModelID {
|
|
||||||
a.app.Model = &m
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmds = append(cmds, a.app.SaveState())
|
|
||||||
case dialog.ThemeSelectedMsg:
|
case dialog.ThemeSelectedMsg:
|
||||||
a.app.State.Theme = msg.ThemeName
|
a.app.State.Theme = msg.ThemeName
|
||||||
cmds = append(cmds, a.app.SaveState())
|
cmds = append(cmds, a.app.SaveState())
|
||||||
@@ -641,8 +700,6 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
// Reset exit key state after timeout
|
// Reset exit key state after timeout
|
||||||
a.exitKeyState = ExitKeyIdle
|
a.exitKeyState = ExitKeyIdle
|
||||||
a.editor.SetExitKeyInDebounce(false)
|
a.editor.SetExitKeyInDebounce(false)
|
||||||
case dialog.FindSelectedMsg:
|
|
||||||
return a.openFile(msg.FilePath)
|
|
||||||
case tea.PasteMsg, tea.ClipboardMsg:
|
case tea.PasteMsg, tea.ClipboardMsg:
|
||||||
// Paste events: prioritize modal if active, otherwise editor
|
// Paste events: prioritize modal if active, otherwise editor
|
||||||
if a.modal != nil {
|
if a.modal != nil {
|
||||||
@@ -666,6 +723,9 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case "/tui/open-sessions":
|
case "/tui/open-sessions":
|
||||||
sessionDialog := dialog.NewSessionDialog(a.app)
|
sessionDialog := dialog.NewSessionDialog(a.app)
|
||||||
a.modal = sessionDialog
|
a.modal = sessionDialog
|
||||||
|
case "/tui/open-navigation":
|
||||||
|
navigationDialog := dialog.NewNavigationDialog(a.app)
|
||||||
|
a.modal = navigationDialog
|
||||||
case "/tui/open-themes":
|
case "/tui/open-themes":
|
||||||
themeDialog := dialog.NewThemeDialog()
|
themeDialog := dialog.NewThemeDialog()
|
||||||
a.modal = themeDialog
|
a.modal = themeDialog
|
||||||
@@ -710,6 +770,45 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
updated, cmd := a.executeCommand(commands.Command(command))
|
updated, cmd := a.executeCommand(commands.Command(command))
|
||||||
a = updated.(Model)
|
a = updated.(Model)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
case "/tui/show-toast":
|
||||||
|
var body struct {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Variant string `json:"variant"`
|
||||||
|
}
|
||||||
|
json.Unmarshal((msg.Body), &body)
|
||||||
|
|
||||||
|
var toastCmd tea.Cmd
|
||||||
|
switch body.Variant {
|
||||||
|
case "info":
|
||||||
|
if body.Title != "" {
|
||||||
|
toastCmd = toast.NewInfoToast(body.Message, toast.WithTitle(body.Title))
|
||||||
|
} else {
|
||||||
|
toastCmd = toast.NewInfoToast(body.Message)
|
||||||
|
}
|
||||||
|
case "success":
|
||||||
|
if body.Title != "" {
|
||||||
|
toastCmd = toast.NewSuccessToast(body.Message, toast.WithTitle(body.Title))
|
||||||
|
} else {
|
||||||
|
toastCmd = toast.NewSuccessToast(body.Message)
|
||||||
|
}
|
||||||
|
case "warning":
|
||||||
|
if body.Title != "" {
|
||||||
|
toastCmd = toast.NewErrorToast(body.Message, toast.WithTitle(body.Title))
|
||||||
|
} else {
|
||||||
|
toastCmd = toast.NewErrorToast(body.Message)
|
||||||
|
}
|
||||||
|
case "error":
|
||||||
|
if body.Title != "" {
|
||||||
|
toastCmd = toast.NewErrorToast(body.Message, toast.WithTitle(body.Title))
|
||||||
|
} else {
|
||||||
|
toastCmd = toast.NewErrorToast(body.Message)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
slog.Error("Invalid toast variant", "variant", body.Variant)
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
cmds = append(cmds, toastCmd)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -741,22 +840,20 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fv, cmd := a.fileViewer.Update(msg)
|
|
||||||
a.fileViewer = fv
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
|
|
||||||
return a, tea.Batch(cmds...)
|
return a, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Model) View() string {
|
func (a Model) View() (string, *tea.Cursor) {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
|
|
||||||
var mainLayout string
|
var mainLayout string
|
||||||
|
|
||||||
|
var editorX int
|
||||||
|
var editorY int
|
||||||
if a.app.Session.ID == "" {
|
if a.app.Session.ID == "" {
|
||||||
mainLayout = a.home()
|
mainLayout, editorX, editorY = a.home()
|
||||||
} else {
|
} else {
|
||||||
mainLayout = a.chat()
|
mainLayout, editorX, editorY = a.chat()
|
||||||
}
|
}
|
||||||
mainLayout = styles.NewStyle().
|
mainLayout = styles.NewStyle().
|
||||||
Background(t.Background()).
|
Background(t.Background()).
|
||||||
@@ -780,34 +877,19 @@ func (a Model) View() string {
|
|||||||
if theme.CurrentThemeUsesAnsiColors() {
|
if theme.CurrentThemeUsesAnsiColors() {
|
||||||
mainLayout = util.ConvertRGBToAnsi16Colors(mainLayout)
|
mainLayout = util.ConvertRGBToAnsi16Colors(mainLayout)
|
||||||
}
|
}
|
||||||
return mainLayout + "\n" + a.status.View()
|
|
||||||
|
cursor := a.editor.Cursor()
|
||||||
|
cursor.Position.X += editorX
|
||||||
|
cursor.Position.Y += editorY
|
||||||
|
|
||||||
|
return mainLayout + "\n" + a.status.View(), cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Model) Cleanup() {
|
func (a Model) Cleanup() {
|
||||||
a.status.Cleanup()
|
a.status.Cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Model) openFile(filepath string) (tea.Model, tea.Cmd) {
|
func (a Model) home() (string, int, int) {
|
||||||
var cmd tea.Cmd
|
|
||||||
response, err := a.app.Client.File.Read(
|
|
||||||
context.Background(),
|
|
||||||
opencode.FileReadParams{
|
|
||||||
Path: opencode.F(filepath),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to read file", "error", err)
|
|
||||||
return a, toast.NewErrorToast("Failed to read file")
|
|
||||||
}
|
|
||||||
a.fileViewer, cmd = a.fileViewer.SetFile(
|
|
||||||
filepath,
|
|
||||||
response.Content,
|
|
||||||
response.Type == "patch",
|
|
||||||
)
|
|
||||||
return a, cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Model) home() string {
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
effectiveWidth := a.width - 4
|
effectiveWidth := a.width - 4
|
||||||
baseStyle := styles.NewStyle().Background(t.Background())
|
baseStyle := styles.NewStyle().Background(t.Background())
|
||||||
@@ -899,14 +981,21 @@ func (a Model) home() string {
|
|||||||
styles.WhitespaceStyle(t.Background()),
|
styles.WhitespaceStyle(t.Background()),
|
||||||
)
|
)
|
||||||
|
|
||||||
editorX := (effectiveWidth - editorWidth) / 2
|
editorX := max(0, (effectiveWidth-editorWidth)/2)
|
||||||
editorY := (a.height / 2) + (mainHeight / 2) - 2
|
editorY := (a.height / 2) + (mainHeight / 2) - 2
|
||||||
|
|
||||||
if editorLines > 1 {
|
if editorLines > 1 {
|
||||||
|
content := a.editor.Content()
|
||||||
|
editorHeight := lipgloss.Height(content)
|
||||||
|
|
||||||
|
if editorY+editorHeight > a.height {
|
||||||
|
difference := (editorY + editorHeight) - a.height
|
||||||
|
editorY -= difference
|
||||||
|
}
|
||||||
mainLayout = layout.PlaceOverlay(
|
mainLayout = layout.PlaceOverlay(
|
||||||
editorX,
|
editorX,
|
||||||
editorY,
|
editorY,
|
||||||
a.editor.Content(),
|
content,
|
||||||
mainLayout,
|
mainLayout,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -924,10 +1013,10 @@ func (a Model) home() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mainLayout
|
return mainLayout, editorX + 5, editorY + 2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Model) chat() string {
|
func (a Model) chat() (string, int, int) {
|
||||||
effectiveWidth := a.width - 4
|
effectiveWidth := a.width - 4
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
editorView := a.editor.View()
|
editorView := a.editor.View()
|
||||||
@@ -944,14 +1033,20 @@ func (a Model) chat() string {
|
|||||||
)
|
)
|
||||||
|
|
||||||
mainLayout := messagesView + "\n" + editorView
|
mainLayout := messagesView + "\n" + editorView
|
||||||
editorX := (effectiveWidth - editorWidth) / 2
|
editorX := max(0, (effectiveWidth-editorWidth)/2)
|
||||||
|
editorY := a.height - editorHeight
|
||||||
|
|
||||||
if lines > 1 {
|
if lines > 1 {
|
||||||
editorY := a.height - editorHeight
|
content := a.editor.Content()
|
||||||
|
editorHeight := lipgloss.Height(content)
|
||||||
|
if editorY+editorHeight > a.height {
|
||||||
|
difference := (editorY + editorHeight) - a.height
|
||||||
|
editorY -= difference
|
||||||
|
}
|
||||||
mainLayout = layout.PlaceOverlay(
|
mainLayout = layout.PlaceOverlay(
|
||||||
editorX,
|
editorX,
|
||||||
editorY,
|
editorY,
|
||||||
a.editor.Content(),
|
content,
|
||||||
mainLayout,
|
mainLayout,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -970,7 +1065,7 @@ func (a Model) chat() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mainLayout
|
return mainLayout, editorX + 5, editorY + 2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
||||||
@@ -982,11 +1077,11 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|||||||
case commands.AppHelpCommand:
|
case commands.AppHelpCommand:
|
||||||
helpDialog := dialog.NewHelpDialog(a.app)
|
helpDialog := dialog.NewHelpDialog(a.app)
|
||||||
a.modal = helpDialog
|
a.modal = helpDialog
|
||||||
case commands.SwitchAgentCommand:
|
case commands.AgentCycleCommand:
|
||||||
updated, cmd := a.app.SwitchAgent()
|
updated, cmd := a.app.SwitchAgent()
|
||||||
a.app = updated
|
a.app = updated
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
case commands.SwitchAgentReverseCommand:
|
case commands.AgentCycleReverseCommand:
|
||||||
updated, cmd := a.app.SwitchAgentReverse()
|
updated, cmd := a.app.SwitchAgentReverse()
|
||||||
a.app = updated
|
a.app = updated
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
@@ -1046,6 +1141,12 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|||||||
case commands.SessionListCommand:
|
case commands.SessionListCommand:
|
||||||
sessionDialog := dialog.NewSessionDialog(a.app)
|
sessionDialog := dialog.NewSessionDialog(a.app)
|
||||||
a.modal = sessionDialog
|
a.modal = sessionDialog
|
||||||
|
case commands.SessionNavigationCommand:
|
||||||
|
if a.app.Session.ID == "" {
|
||||||
|
return a, toast.NewErrorToast("No active session")
|
||||||
|
}
|
||||||
|
navigationDialog := dialog.NewNavigationDialog(a.app)
|
||||||
|
a.modal = navigationDialog
|
||||||
case commands.SessionShareCommand:
|
case commands.SessionShareCommand:
|
||||||
if a.app.Session.ID == "" {
|
if a.app.Session.ID == "" {
|
||||||
return a, nil
|
return a, nil
|
||||||
@@ -1081,6 +1182,122 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
// TODO: block until compaction is complete
|
// TODO: block until compaction is complete
|
||||||
a.app.CompactSession(context.Background())
|
a.app.CompactSession(context.Background())
|
||||||
|
case commands.SessionChildCycleCommand:
|
||||||
|
if a.app.Session.ID == "" {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
cmds = append(cmds, func() tea.Msg {
|
||||||
|
parentSessionID := a.app.Session.ID
|
||||||
|
var parentSession *opencode.Session
|
||||||
|
if a.app.Session.ParentID != "" {
|
||||||
|
parentSessionID = a.app.Session.ParentID
|
||||||
|
session, err := a.app.Client.Session.Get(context.Background(), parentSessionID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get parent session", "error", err)
|
||||||
|
return toast.NewErrorToast("Failed to get parent session")
|
||||||
|
}
|
||||||
|
parentSession = session
|
||||||
|
} else {
|
||||||
|
parentSession = a.app.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
children, err := a.app.Client.Session.Children(context.Background(), parentSessionID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get session children", "error", err)
|
||||||
|
return toast.NewErrorToast("Failed to get session children")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse sort the children (newest first)
|
||||||
|
slices.Reverse(*children)
|
||||||
|
|
||||||
|
// Create combined array: [parent, child1, child2, ...]
|
||||||
|
sessions := []*opencode.Session{parentSession}
|
||||||
|
for i := range *children {
|
||||||
|
sessions = append(sessions, &(*children)[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sessions) == 1 {
|
||||||
|
return toast.NewInfoToast("No child sessions available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find current session index in combined array
|
||||||
|
currentIndex := -1
|
||||||
|
for i, session := range sessions {
|
||||||
|
if session.ID == a.app.Session.ID {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If session not found, default to parent (shouldn't happen)
|
||||||
|
if currentIndex == -1 {
|
||||||
|
currentIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle to next session (parent or child)
|
||||||
|
nextIndex := (currentIndex + 1) % len(sessions)
|
||||||
|
nextSession := sessions[nextIndex]
|
||||||
|
|
||||||
|
return app.SessionSelectedMsg(nextSession)
|
||||||
|
})
|
||||||
|
case commands.SessionChildCycleReverseCommand:
|
||||||
|
if a.app.Session.ID == "" {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
cmds = append(cmds, func() tea.Msg {
|
||||||
|
parentSessionID := a.app.Session.ID
|
||||||
|
var parentSession *opencode.Session
|
||||||
|
if a.app.Session.ParentID != "" {
|
||||||
|
parentSessionID = a.app.Session.ParentID
|
||||||
|
session, err := a.app.Client.Session.Get(context.Background(), parentSessionID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get parent session", "error", err)
|
||||||
|
return toast.NewErrorToast("Failed to get parent session")
|
||||||
|
}
|
||||||
|
parentSession = session
|
||||||
|
} else {
|
||||||
|
parentSession = a.app.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
children, err := a.app.Client.Session.Children(context.Background(), parentSessionID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get session children", "error", err)
|
||||||
|
return toast.NewErrorToast("Failed to get session children")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse sort the children (newest first)
|
||||||
|
slices.Reverse(*children)
|
||||||
|
|
||||||
|
// Create combined array: [parent, child1, child2, ...]
|
||||||
|
sessions := []*opencode.Session{parentSession}
|
||||||
|
for i := range *children {
|
||||||
|
sessions = append(sessions, &(*children)[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sessions) == 1 {
|
||||||
|
return toast.NewInfoToast("No child sessions available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find current session index in combined array
|
||||||
|
currentIndex := -1
|
||||||
|
for i, session := range sessions {
|
||||||
|
if session.ID == a.app.Session.ID {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If session not found, default to parent (shouldn't happen)
|
||||||
|
if currentIndex == -1 {
|
||||||
|
currentIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle to previous session (parent or child)
|
||||||
|
nextIndex := (currentIndex - 1 + len(sessions)) % len(sessions)
|
||||||
|
nextSession := sessions[nextIndex]
|
||||||
|
|
||||||
|
return app.SessionSelectedMsg(nextSession)
|
||||||
|
})
|
||||||
case commands.SessionExportCommand:
|
case commands.SessionExportCommand:
|
||||||
if a.app.Session.ID == "" {
|
if a.app.Session.ID == "" {
|
||||||
return a, toast.NewErrorToast("No active session to export.")
|
return a, toast.NewErrorToast("No active session to export.")
|
||||||
@@ -1149,6 +1366,7 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|||||||
case commands.ModelListCommand:
|
case commands.ModelListCommand:
|
||||||
modelDialog := dialog.NewModelDialog(a.app)
|
modelDialog := dialog.NewModelDialog(a.app)
|
||||||
a.modal = modelDialog
|
a.modal = modelDialog
|
||||||
|
|
||||||
case commands.AgentListCommand:
|
case commands.AgentListCommand:
|
||||||
agentDialog := dialog.NewAgentDialog(a.app)
|
agentDialog := dialog.NewAgentDialog(a.app)
|
||||||
a.modal = agentDialog
|
a.modal = agentDialog
|
||||||
@@ -1157,24 +1375,13 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|||||||
updated, cmd := a.app.CycleRecentModel()
|
updated, cmd := a.app.CycleRecentModel()
|
||||||
a.app = updated
|
a.app = updated
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
case commands.ModelCycleRecentReverseCommand:
|
||||||
|
updated, cmd := a.app.CycleRecentModelReverse()
|
||||||
|
a.app = updated
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
case commands.ThemeListCommand:
|
case commands.ThemeListCommand:
|
||||||
themeDialog := dialog.NewThemeDialog()
|
themeDialog := dialog.NewThemeDialog()
|
||||||
a.modal = themeDialog
|
a.modal = themeDialog
|
||||||
// case commands.FileListCommand:
|
|
||||||
// a.editor.Blur()
|
|
||||||
// findDialog := dialog.NewFindDialog(a.fileProvider)
|
|
||||||
// cmds = append(cmds, findDialog.Init())
|
|
||||||
// a.modal = findDialog
|
|
||||||
case commands.FileCloseCommand:
|
|
||||||
a.fileViewer, cmd = a.fileViewer.Clear()
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
case commands.FileDiffToggleCommand:
|
|
||||||
a.fileViewer, cmd = a.fileViewer.ToggleDiff()
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
a.app.State.SplitDiff = a.fileViewer.DiffStyle() == fileviewer.DiffStyleSplit
|
|
||||||
cmds = append(cmds, a.app.SaveState())
|
|
||||||
case commands.FileSearchCommand:
|
|
||||||
return a, nil
|
|
||||||
case commands.ProjectInitCommand:
|
case commands.ProjectInitCommand:
|
||||||
cmds = append(cmds, a.app.InitializeProject(context.Background()))
|
cmds = append(cmds, a.app.InitializeProject(context.Background()))
|
||||||
case commands.InputClearCommand:
|
case commands.InputClearCommand:
|
||||||
@@ -1205,45 +1412,21 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|||||||
a.messages = updated.(chat.MessagesComponent)
|
a.messages = updated.(chat.MessagesComponent)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
case commands.MessagesPageUpCommand:
|
case commands.MessagesPageUpCommand:
|
||||||
if a.fileViewer.HasFile() {
|
updated, cmd := a.messages.PageUp()
|
||||||
a.fileViewer, cmd = a.fileViewer.PageUp()
|
a.messages = updated.(chat.MessagesComponent)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
} else {
|
|
||||||
updated, cmd := a.messages.PageUp()
|
|
||||||
a.messages = updated.(chat.MessagesComponent)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
case commands.MessagesPageDownCommand:
|
case commands.MessagesPageDownCommand:
|
||||||
if a.fileViewer.HasFile() {
|
updated, cmd := a.messages.PageDown()
|
||||||
a.fileViewer, cmd = a.fileViewer.PageDown()
|
a.messages = updated.(chat.MessagesComponent)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
} else {
|
|
||||||
updated, cmd := a.messages.PageDown()
|
|
||||||
a.messages = updated.(chat.MessagesComponent)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
case commands.MessagesHalfPageUpCommand:
|
case commands.MessagesHalfPageUpCommand:
|
||||||
if a.fileViewer.HasFile() {
|
updated, cmd := a.messages.HalfPageUp()
|
||||||
a.fileViewer, cmd = a.fileViewer.HalfPageUp()
|
a.messages = updated.(chat.MessagesComponent)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
} else {
|
|
||||||
updated, cmd := a.messages.HalfPageUp()
|
|
||||||
a.messages = updated.(chat.MessagesComponent)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
case commands.MessagesHalfPageDownCommand:
|
case commands.MessagesHalfPageDownCommand:
|
||||||
if a.fileViewer.HasFile() {
|
updated, cmd := a.messages.HalfPageDown()
|
||||||
a.fileViewer, cmd = a.fileViewer.HalfPageDown()
|
a.messages = updated.(chat.MessagesComponent)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
} else {
|
|
||||||
updated, cmd := a.messages.HalfPageDown()
|
|
||||||
a.messages = updated.(chat.MessagesComponent)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
case commands.MessagesLayoutToggleCommand:
|
|
||||||
a.messagesRight = !a.messagesRight
|
|
||||||
a.app.State.MessagesRight = a.messagesRight
|
|
||||||
cmds = append(cmds, a.app.SaveState())
|
|
||||||
case commands.MessagesCopyCommand:
|
case commands.MessagesCopyCommand:
|
||||||
updated, cmd := a.messages.CopyLastMessage()
|
updated, cmd := a.messages.CopyLastMessage()
|
||||||
a.messages = updated.(chat.MessagesComponent)
|
a.messages = updated.(chat.MessagesComponent)
|
||||||
@@ -1293,8 +1476,6 @@ func NewModel(app *app.App) tea.Model {
|
|||||||
toastManager: toast.NewToastManager(),
|
toastManager: toast.NewToastManager(),
|
||||||
interruptKeyState: InterruptKeyIdle,
|
interruptKeyState: InterruptKeyIdle,
|
||||||
exitKeyState: ExitKeyIdle,
|
exitKeyState: ExitKeyIdle,
|
||||||
fileViewer: fileviewer.New(app),
|
|
||||||
messagesRight: app.State.MessagesRight,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package util
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -85,6 +86,8 @@ func Extension(path string) string {
|
|||||||
func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
|
func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
|
||||||
r := styles.GetMarkdownRenderer(width-6, backgroundColor)
|
r := styles.GetMarkdownRenderer(width-6, backgroundColor)
|
||||||
content = strings.ReplaceAll(content, RootPath+"/", "")
|
content = strings.ReplaceAll(content, RootPath+"/", "")
|
||||||
|
hyphenRegex := regexp.MustCompile(`-([^ \-|]|$)`)
|
||||||
|
content = hyphenRegex.ReplaceAllString(content, "\u2011$1")
|
||||||
rendered, _ := r.Render(content)
|
rendered, _ := r.Render(content)
|
||||||
lines := strings.Split(rendered, "\n")
|
lines := strings.Split(rendered, "\n")
|
||||||
|
|
||||||
@@ -105,5 +108,6 @@ func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
content = strings.Join(lines, "\n")
|
content = strings.Join(lines, "\n")
|
||||||
|
content = strings.ReplaceAll(content, "\u2011", "-")
|
||||||
return strings.TrimSuffix(content, "\n")
|
return strings.TrimSuffix(content, "\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var SUPPORTED_IDES = []struct {
|
var SUPPORTED_IDES = []struct {
|
||||||
Search string
|
Search string
|
||||||
ShortName string
|
ShortName string
|
||||||
}{
|
}{
|
||||||
{"Windsurf", "Windsurf"},
|
{"Windsurf", "Windsurf"},
|
||||||
{"Visual Studio Code", "VS Code"},
|
{"Visual Studio Code", "vscode"},
|
||||||
{"Cursor", "Cursor"},
|
{"Cursor", "Cursor"},
|
||||||
{"VSCodium", "VSCodium"},
|
{"VSCodium", "VSCodium"},
|
||||||
}
|
}
|
||||||
@@ -27,4 +27,5 @@ func Ide() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode/web",
|
"name": "@opencode/web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.4.26",
|
"version": "0.5.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
|
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 902 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 998 KiB |
@@ -5,7 +5,9 @@ import type { Props } from '@astrojs/starlight/props';
|
|||||||
|
|
||||||
import CopyIcon from "../assets/lander/copy.svg";
|
import CopyIcon from "../assets/lander/copy.svg";
|
||||||
import CheckIcon from "../assets/lander/check.svg";
|
import CheckIcon from "../assets/lander/check.svg";
|
||||||
import Screenshot from "../assets/lander/screenshot-splash.png";
|
import TuiScreenshot from "../assets/lander/screenshot-splash.png";
|
||||||
|
import VscodeScreenshot from "../assets/lander/screenshot-vscode.png";
|
||||||
|
import GithubScreenshot from "../assets/lander/screenshot-github.png";
|
||||||
|
|
||||||
const { data } = Astro.locals.starlightRoute.entry;
|
const { data } = Astro.locals.starlightRoute.entry;
|
||||||
const { title = data.title, tagline, image, actions = [] } = data.hero || {};
|
const { title = data.title, tagline, image, actions = [] } = data.hero || {};
|
||||||
@@ -18,6 +20,7 @@ const imageAttrs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const github = config.social.filter(s => s.icon === 'github')[0];
|
const github = config.social.filter(s => s.icon === 'github')[0];
|
||||||
|
const discord = config.social.filter(s => s.icon === 'discord')[0];
|
||||||
|
|
||||||
const command = "curl -fsSL"
|
const command = "curl -fsSL"
|
||||||
const protocol = "https://"
|
const protocol = "https://"
|
||||||
@@ -35,7 +38,7 @@ if (image) {
|
|||||||
lightImage = image.light;
|
lightImage = image.light;
|
||||||
} else {
|
} else {
|
||||||
rawHtml = image.html;
|
rawHtml = image.html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
@@ -53,7 +56,7 @@ if (image) {
|
|||||||
|
|
||||||
<section class="cta">
|
<section class="cta">
|
||||||
<div class="col1">
|
<div class="col1">
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Get Started</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col2">
|
<div class="col2">
|
||||||
<button class="command" data-command={`${command} ${protocol}${url} ${bash}`}>
|
<button class="command" data-command={`${command} ${protocol}${url} ${bash}`}>
|
||||||
@@ -66,9 +69,6 @@ if (image) {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col3">
|
|
||||||
<a href={github.href}>GitHub</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="content">
|
<section class="content">
|
||||||
@@ -82,19 +82,95 @@ if (image) {
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="alternatives">
|
||||||
|
<div class="col1">
|
||||||
|
<h3>npm</h3>
|
||||||
|
<button class="command" data-command="npm install -g opencode-ai">
|
||||||
|
<code>
|
||||||
|
<span>npm install -g</span> <span class="highlight">opencode-ai</span>
|
||||||
|
</code>
|
||||||
|
<span class="copy">
|
||||||
|
<CopyIcon />
|
||||||
|
<CheckIcon />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col2">
|
||||||
|
<h3>Bun</h3>
|
||||||
|
<button class="command" data-command="bun install -g opencode-ai">
|
||||||
|
<code>
|
||||||
|
<span>bun install -g</span> <span class="highlight">opencode-ai</span>
|
||||||
|
</code>
|
||||||
|
<span class="copy">
|
||||||
|
<CopyIcon />
|
||||||
|
<CheckIcon />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col3">
|
||||||
|
<h3>Homebrew</h3>
|
||||||
|
<button class="command" data-command="brew install sst/tap/opencode">
|
||||||
|
<code>
|
||||||
|
<span>brew install</span> <span class="highlight">sst/tap/opencode</span>
|
||||||
|
</code>
|
||||||
|
<span class="copy">
|
||||||
|
<CopyIcon />
|
||||||
|
<CheckIcon />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col4">
|
||||||
|
<h3>Paru</h3>
|
||||||
|
<button class="command" data-command="paru -S opencode-bin">
|
||||||
|
<code>
|
||||||
|
<span>paru -S</span> <span class="highlight">opencode-bin</span>
|
||||||
|
</code>
|
||||||
|
<span class="copy">
|
||||||
|
<CopyIcon />
|
||||||
|
<CheckIcon />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="images">
|
<section class="images">
|
||||||
<div>
|
<div class="left">
|
||||||
<p>opencode TUI with the tokyonight theme</p>
|
<figure>
|
||||||
<Image width={600} src={Screenshot} alt="opencode TUI with the tokyonight theme" />
|
<figcaption>opencode TUI with the tokyonight theme</figcaption>
|
||||||
|
<a href="/docs/cli">
|
||||||
|
<Image src={TuiScreenshot} alt="opencode TUI with the tokyonight theme" />
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<div class="row1">
|
||||||
|
<figure>
|
||||||
|
<figcaption>opencode in VS Code</figcaption>
|
||||||
|
<a href="/docs/ide">
|
||||||
|
<Image src={VscodeScreenshot} alt="opencode in VS Code" />
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="row2">
|
||||||
|
<figure>
|
||||||
|
<figcaption>opencode in GitHub</figcaption>
|
||||||
|
<a href="/docs/github">
|
||||||
|
<Image src={GithubScreenshot} alt="opencode in GitHub" />
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="footer">
|
<section class="footer">
|
||||||
<div class="col1">
|
<div class="col1">
|
||||||
<span>Version: Beta</span>
|
<a href={github.href} target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col2">
|
<div class="col2">
|
||||||
<span>Author: <a href="https://sst.dev">SST</a></span>
|
<a href={discord.href} target="_blank" rel="noopener noreferrer">Discord</a>
|
||||||
|
</div>
|
||||||
|
<div class="col3">
|
||||||
|
<span>©2025 <a href="https://anoma.ly" target="_blank" rel="noopener noreferrer">Anomaly Innovations</a></span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +178,7 @@ if (image) {
|
|||||||
<style>
|
<style>
|
||||||
.hero {
|
.hero {
|
||||||
--padding: 3rem;
|
--padding: 3rem;
|
||||||
--vertical-padding: 2rem;
|
--vertical-padding: 1.5rem;
|
||||||
--heading-font-size: var(--sl-text-3xl);
|
--heading-font-size: var(--sl-text-3xl);
|
||||||
|
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
@@ -111,7 +187,7 @@ if (image) {
|
|||||||
@media (max-width: 30rem) {
|
@media (max-width: 30rem) {
|
||||||
.hero {
|
.hero {
|
||||||
--padding: 1rem;
|
--padding: 1rem;
|
||||||
--vertical-padding: 1rem;
|
--vertical-padding: 0.75rem;
|
||||||
--heading-font-size: var(--sl-text-2xl);
|
--heading-font-size: var(--sl-text-2xl);
|
||||||
|
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
@@ -143,31 +219,29 @@ section.cta {
|
|||||||
|
|
||||||
@media (max-width: 50rem) {
|
@media (max-width: 50rem) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
& > div.col1 { order: 1; }
|
|
||||||
& > div.col3 { order: 2; }
|
|
||||||
& > div.col2 { order: 3; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
padding: calc(var(--padding) / 2) 1rem;
|
padding: var(--vertical-padding) var(--padding);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& > div.col1, & > div.col3 {
|
& > div.col1 {
|
||||||
flex: 1 1 auto;
|
flex: 0 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
@media (max-width: 50rem) {
|
@media (max-width: 50rem) {
|
||||||
padding-bottom: calc(var(--padding) / 2 + 4px);
|
padding-bottom: calc(var(--vertical-padding) + 4px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& > div.col2 {
|
& > div.col2 {
|
||||||
flex: 0 0 auto;
|
flex: 1;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div + div {
|
& > div + div {
|
||||||
@@ -261,13 +335,40 @@ section.content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
section.images {
|
section.images {
|
||||||
display: flex;
|
--images-height: 600px;
|
||||||
flex-direction: row;
|
display: grid;
|
||||||
align-items: stretch;
|
grid-template-columns: 1fr 1fr;
|
||||||
justify-content: space-between;
|
grid-template-rows: var(--images-height);
|
||||||
border-top: 2px solid var(--sl-color-border);
|
border-top: 2px solid var(--sl-color-border);
|
||||||
|
|
||||||
& > div {
|
& > div.left {
|
||||||
|
display: flex;
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.right {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 2;
|
||||||
|
border-left: 2px solid var(--sl-color-border);
|
||||||
|
|
||||||
|
& > div.row1 {
|
||||||
|
display: flex;
|
||||||
|
grid-row: 1;
|
||||||
|
border-bottom: 2px solid var(--sl-color-border);
|
||||||
|
height: calc(var(--images-height) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.row2 {
|
||||||
|
display: flex;
|
||||||
|
grid-row: 2;
|
||||||
|
height: calc(var(--images-height) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -276,51 +377,245 @@ section.images {
|
|||||||
border-width: 0;
|
border-width: 0;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: var(--sl-color-border);
|
border-color: var(--sl-color-border);
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
& > div, p {
|
& > div, figcaption {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--sl-color-text-dimmed);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
& > div.left figure {
|
||||||
|
height: var(--images-height);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.right figure {
|
||||||
|
height: calc(var(--images-height) / 2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.left img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.right img {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 2rem);
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
& {
|
||||||
|
--images-height: auto;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.left {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.right {
|
||||||
|
grid-row: 2;
|
||||||
|
grid-column: 1;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 2px solid var(--sl-color-border);
|
||||||
|
|
||||||
|
& > div.row1,
|
||||||
|
& > div.row2 {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.left figure,
|
||||||
|
& > div.right figure {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.left img,
|
||||||
|
& > div.right img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.alternatives {
|
||||||
|
border-top: 2px solid var(--sl-color-border);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: calc(var(--vertical-padding) / 2) calc(var(--padding) / 2) calc(var(--vertical-padding) / 2 + 0.125rem);
|
||||||
|
text-align: left;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
gap: 0.3125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.col1 {
|
||||||
|
border-bottom: 2px solid var(--sl-color-border);
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.col2 {
|
||||||
|
border-left: 2px solid var(--sl-color-border);
|
||||||
|
border-bottom: 2px solid var(--sl-color-border);
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
border-left: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top: 2px solid var(--sl-color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.col3 {
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
border-top: 2px solid var(--sl-color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.col4 {
|
||||||
|
border-left: 2px solid var(--sl-color-border);
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
border-left: none;
|
||||||
|
border-top: 2px solid var(--sl-color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
letter-spacing: -0.03125rem;
|
letter-spacing: -0.03125rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--sl-color-text-dimmed);
|
color: var(--sl-color-text-dimmed);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
@media (max-width: 30rem) {
|
@media (max-width: 30rem) {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
.command {
|
||||||
align-self: center;
|
all: unset;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.625rem;
|
||||||
|
justify-content: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > div + div {
|
@media (max-width: 40rem) {
|
||||||
border-width: 0 0 0 2px;
|
justify-content: flex-start;
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 30rem) {
|
|
||||||
& {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
& > div + div {
|
|
||||||
border-width: 2px 0 0 0;
|
@media (max-width: 30rem) {
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section.approach {
|
code {
|
||||||
border-top: 2px solid var(--sl-color-border);
|
color: var(--sl-color-text-secondary);
|
||||||
padding: var(--padding);
|
font-size: 1rem;
|
||||||
|
|
||||||
p + p {
|
@media (max-width: 24rem) {
|
||||||
margin-top: var(--vertical-padding);
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code .highlight {
|
||||||
|
color: var(--sl-color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy svg:first-child {
|
||||||
|
color: var(--sl-color-text-dimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy svg:last-child {
|
||||||
|
color: var(--sl-color-text);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success .copy {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success .copy svg:first-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success .copy svg:last-child {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,12 +628,42 @@ section.footer {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
padding: calc(var(--padding) / 2) 0.5rem;
|
padding: var(--vertical-padding) 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div + div {
|
& > div + div {
|
||||||
border-left: 2px solid var(--sl-color-border);
|
border-left: 2px solid var(--sl-color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Below 800px: first two columns shrink to content, third expands */
|
||||||
|
@media (max-width: 50rem) {
|
||||||
|
& > div.col1,
|
||||||
|
& > div.col2 {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding-left: calc(var(--padding) / 2);
|
||||||
|
padding-right: calc(var(--padding) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.col3 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile: third column on its own row */
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& > div.col1,
|
||||||
|
& > div.col2 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.col3 {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 2px solid var(--sl-color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -361,13 +686,15 @@ section.footer {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const button = document.querySelector("button.command") as HTMLButtonElement;
|
const buttons = document.querySelectorAll("button.command") as NodeListOf<HTMLButtonElement>
|
||||||
|
|
||||||
button?.addEventListener("click", () => {
|
buttons.forEach(button => {
|
||||||
navigator.clipboard.writeText(button.dataset.command!);
|
button.addEventListener("click", () => {
|
||||||
button.classList.toggle("success");
|
navigator.clipboard.writeText(button.dataset.command!)
|
||||||
setTimeout(() => {
|
button.classList.toggle("success")
|
||||||
button.classList.toggle("success");
|
setTimeout(() => {
|
||||||
}, 1500);
|
button.classList.toggle("success");
|
||||||
});
|
}, 1500)
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -90,6 +90,13 @@ A general-purpose agent for researching complex questions, searching for code, a
|
|||||||
@general help me search for this function
|
@general help me search for this function
|
||||||
```
|
```
|
||||||
|
|
||||||
|
3. **Navigation between sessions**: When subagents create their own child sessions, you can navigate between the parent session and all child sessions using:
|
||||||
|
|
||||||
|
- **Ctrl+Right** (or your configured `session_child_cycle` keybind) to cycle forward through parent → child1 → child2 → ... → parent
|
||||||
|
- **Ctrl+Left** (or your configured `session_child_cycle_reverse` keybind) to cycle backward through parent ← child1 ← child2 ← ... ← parent
|
||||||
|
|
||||||
|
This allows you to seamlessly switch between the main conversation and specialized subagent work.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configure
|
## Configure
|
||||||
@@ -318,6 +325,22 @@ Control which tools are available in this agent with the `tools` config. You can
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also use wildcards to control multiple tools at once. For example, to disable all tools from an MCP server:
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"agent": {
|
||||||
|
"readonly": {
|
||||||
|
"tools": {
|
||||||
|
"mymcp_*": false,
|
||||||
|
"write": false,
|
||||||
|
"edit": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
If no tools are specified, all tools are enabled by default.
|
If no tools are specified, all tools are enabled by default.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -342,6 +365,147 @@ Here are all the tools can be controlled through the agent config.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
|
||||||
|
Permissions control what actions an agent can take.
|
||||||
|
|
||||||
|
- edit, bash, webfetch
|
||||||
|
|
||||||
|
Each permission can be set to allow, ask, or deny.
|
||||||
|
|
||||||
|
- allow, ask, deny
|
||||||
|
|
||||||
|
Configure permissions globally in opencode.json.
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"permission": {
|
||||||
|
"edit": "ask",
|
||||||
|
"bash": "allow",
|
||||||
|
"webfetch": "deny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can override permissions per agent in JSON.
|
||||||
|
|
||||||
|
```json title="opencode.json" {7-18}
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"agent": {
|
||||||
|
"build": {
|
||||||
|
"permission": {
|
||||||
|
"edit": "allow",
|
||||||
|
"bash": {
|
||||||
|
"*": "allow",
|
||||||
|
"git push": "ask",
|
||||||
|
"terraform *": "deny"
|
||||||
|
},
|
||||||
|
"webfetch": "ask"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set permissions in Markdown agents.
|
||||||
|
|
||||||
|
```markdown title="~/.config/opencode/agent/review.md"
|
||||||
|
---
|
||||||
|
description: Code review without edits
|
||||||
|
mode: subagent
|
||||||
|
permission:
|
||||||
|
edit: deny
|
||||||
|
bash: ask
|
||||||
|
webfetch: deny
|
||||||
|
---
|
||||||
|
|
||||||
|
Only analyze code and suggest changes.
|
||||||
|
```
|
||||||
|
|
||||||
|
Bash permissions support granular patterns for fine-grained control.
|
||||||
|
|
||||||
|
```json title="Allow most, ask for risky, deny terraform"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"permission": {
|
||||||
|
"bash": {
|
||||||
|
"*": "allow",
|
||||||
|
"git push": "ask",
|
||||||
|
"terraform *": "deny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you provide a granular bash map, the default becomes ask unless you set \* explicitly.
|
||||||
|
|
||||||
|
```json title="Granular defaults to ask"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"permission": {
|
||||||
|
"bash": {
|
||||||
|
"git status": "allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent-level permissions merge over global settings.
|
||||||
|
|
||||||
|
- Global sets defaults; agent overrides when specified
|
||||||
|
|
||||||
|
Specific bash rules can override a global default.
|
||||||
|
|
||||||
|
```json title="Global ask, agent allows safe commands"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"permission": { "bash": "ask" },
|
||||||
|
"agent": {
|
||||||
|
"build": {
|
||||||
|
"permission": {
|
||||||
|
"bash": { "git status": "allow", "*": "ask" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Permissions affect tool availability and prompts differently.
|
||||||
|
|
||||||
|
- deny hides tools (edit also hides write/patch); ask prompts; allow runs
|
||||||
|
|
||||||
|
For quick reference, here are common setups.
|
||||||
|
|
||||||
|
```json title="Read-only reviewer"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"agent": {
|
||||||
|
"review": {
|
||||||
|
"permission": { "edit": "deny", "bash": "deny", "webfetch": "allow" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json title="Planning agent that can browse but cannot change code"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"agent": {
|
||||||
|
"plan": {
|
||||||
|
"permission": { "edit": "deny", "bash": "deny", "webfetch": "ask" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See the full permissions guide for more patterns.
|
||||||
|
|
||||||
|
- /docs/permissions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Mode
|
### Mode
|
||||||
|
|
||||||
Control the agent's mode with the `mode` config. The `mode` option is used to determine how the agent can be used.
|
Control the agent's mode with the `mode` config. The `mode` option is used to determine how the agent can be used.
|
||||||
@@ -371,7 +535,7 @@ For example, with OpenAI's reasoning models, you can control the reasoning effor
|
|||||||
"agent": {
|
"agent": {
|
||||||
"deep-thinker": {
|
"deep-thinker": {
|
||||||
"description": "Agent that uses high reasoning effort for complex problems",
|
"description": "Agent that uses high reasoning effort for complex problems",
|
||||||
"model": "openai/gpt-5-turbo",
|
"model": "openai/gpt-5-turbo",
|
||||||
"reasoningEffort": "high",
|
"reasoningEffort": "high",
|
||||||
"textVerbosity": "low"
|
"textVerbosity": "low"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,16 @@ Here are all available slash commands:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Bash commands
|
||||||
|
|
||||||
|
Start a line with `!` to run a shell command. The output is added to the conversation as a tool result.
|
||||||
|
|
||||||
|
```bash frame="none"
|
||||||
|
!ls -la
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Editor setup
|
### Editor setup
|
||||||
|
|
||||||
Both the `/editor` and `/export` commands use the editor specified in your `EDITOR` environment variable.
|
Both the `/editor` and `/export` commands use the editor specified in your `EDITOR` environment variable.
|
||||||
@@ -232,10 +242,10 @@ opencode github run
|
|||||||
|
|
||||||
##### Flags
|
##### Flags
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
| --------- | ------------------------------------------------ |
|
| --------- | -------------------------------------- |
|
||||||
| `--event` | GitHub mock event to run the agent for |
|
| `--event` | GitHub mock event to run the agent for |
|
||||||
| `--token` | GitHub personal access token |
|
| `--token` | GitHub personal access token |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -289,9 +299,9 @@ This starts an HTTP server that provides API access to opencode functionality wi
|
|||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
|
|
||||||
| Flag | Short | Description |
|
| Flag | Short | Description |
|
||||||
| ------------ | ----- | ------------------------------------------ |
|
| ------------ | ----- | --------------------- |
|
||||||
| `--port` | `-p` | Port to listen on |
|
| `--port` | `-p` | Port to listen on |
|
||||||
| `--hostname` | `-h` | Hostname to listen on |
|
| `--hostname` | `-h` | Hostname to listen on |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -318,8 +328,8 @@ opencode upgrade v0.1.48
|
|||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
|
|
||||||
| Flag | Short | Description |
|
| Flag | Short | Description |
|
||||||
| ---------- | ----- | --------------------------------------------------------- |
|
| ---------- | ----- | ----------------------------------------------------------------- |
|
||||||
| `--method` | `-m` | The installation method that was used; curl, npm, pnpm, bun, brew |
|
| `--method` | `-m` | The installation method that was used; curl, npm, pnpm, bun, brew |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -337,5 +347,5 @@ The opencode CLI takes the following global flags.
|
|||||||
| `--prompt` | `-p` | Prompt to use |
|
| `--prompt` | `-p` | Prompt to use |
|
||||||
| `--model` | `-m` | Model to use in the form of provider/model |
|
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||||
| `--mode` | | Mode to use |
|
| `--mode` | | Mode to use |
|
||||||
| `--port` | | Port to listen on |
|
| `--port` | | Port to listen on |
|
||||||
| `--hostname` | | Hostname to listen on |
|
| `--hostname` | | Hostname to listen on |
|
||||||
|
|||||||
@@ -11,27 +11,22 @@ opencode has a list of keybinds that you can customize through the opencode conf
|
|||||||
"keybinds": {
|
"keybinds": {
|
||||||
"leader": "ctrl+x",
|
"leader": "ctrl+x",
|
||||||
"app_help": "<leader>h",
|
"app_help": "<leader>h",
|
||||||
"switch_agent": "tab",
|
"app_exit": "ctrl+c,<leader>q",
|
||||||
|
|
||||||
"editor_open": "<leader>e",
|
"editor_open": "<leader>e",
|
||||||
|
|
||||||
"session_new": "<leader>n",
|
|
||||||
"session_list": "<leader>l",
|
|
||||||
"session_share": "<leader>s",
|
|
||||||
"session_unshare": "<leader>u",
|
|
||||||
"session_interrupt": "esc",
|
|
||||||
"session_compact": "<leader>c",
|
|
||||||
|
|
||||||
"tool_details": "<leader>d",
|
|
||||||
"model_list": "<leader>m",
|
|
||||||
"theme_list": "<leader>t",
|
"theme_list": "<leader>t",
|
||||||
"project_init": "<leader>i",
|
"project_init": "<leader>i",
|
||||||
|
"tool_details": "<leader>d",
|
||||||
"input_clear": "ctrl+c",
|
"thinking_blocks": "<leader>b",
|
||||||
"input_paste": "ctrl+v",
|
"session_export": "<leader>x",
|
||||||
"input_submit": "enter",
|
"session_new": "<leader>n",
|
||||||
"input_newline": "shift+enter,ctrl+j",
|
"session_list": "<leader>l",
|
||||||
|
"session_navigation": "<leader>g",
|
||||||
|
"session_share": "<leader>s",
|
||||||
|
"session_unshare": "none",
|
||||||
|
"session_interrupt": "esc",
|
||||||
|
"session_compact": "<leader>c",
|
||||||
|
"session_child_cycle": "ctrl+right",
|
||||||
|
"session_child_cycle_reverse": "ctrl+left",
|
||||||
"messages_page_up": "pgup",
|
"messages_page_up": "pgup",
|
||||||
"messages_page_down": "pgdown",
|
"messages_page_down": "pgdown",
|
||||||
"messages_half_page_up": "ctrl+alt+u",
|
"messages_half_page_up": "ctrl+alt+u",
|
||||||
@@ -39,8 +34,18 @@ opencode has a list of keybinds that you can customize through the opencode conf
|
|||||||
"messages_first": "ctrl+g",
|
"messages_first": "ctrl+g",
|
||||||
"messages_last": "ctrl+alt+g",
|
"messages_last": "ctrl+alt+g",
|
||||||
"messages_copy": "<leader>y",
|
"messages_copy": "<leader>y",
|
||||||
|
"messages_undo": "<leader>u",
|
||||||
"app_exit": "ctrl+c,<leader>q"
|
"messages_redo": "<leader>r",
|
||||||
|
"model_list": "<leader>m",
|
||||||
|
"model_cycle_recent": "f2",
|
||||||
|
"model_cycle_recent_reverse": "shift+f2",
|
||||||
|
"agent_list": "<leader>a",
|
||||||
|
"agent_cycle": "tab",
|
||||||
|
"agent_cycle_reverse": "shift+tab",
|
||||||
|
"input_clear": "ctrl+c",
|
||||||
|
"input_paste": "ctrl+v",
|
||||||
|
"input_submit": "enter",
|
||||||
|
"input_newline": "shift+enter,ctrl+j"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -65,7 +70,7 @@ You can disable a keybind by adding the key to your config with a value of "none
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"keybinds": {
|
"keybinds": {
|
||||||
"session_compact": "none",
|
"session_compact": "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -64,6 +64,36 @@ If you've configured a [custom provider](/docs/providers#custom), the `provider_
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Configure models
|
||||||
|
|
||||||
|
You can globally configure a model's options through the config.
|
||||||
|
|
||||||
|
```jsonc title="opencode.jsonc" {7-11}
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": {
|
||||||
|
"openai": {
|
||||||
|
"models": {
|
||||||
|
"gpt-5": {
|
||||||
|
"options": {
|
||||||
|
"reasoningEffort": "high",
|
||||||
|
"textVerbosity": "low",
|
||||||
|
"reasoningSummary": "auto",
|
||||||
|
"include": ["reasoning.encrypted_content"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we are setting global options for the `gpt-5` model when used through the `openai` provider.
|
||||||
|
|
||||||
|
You can also configure these options for any agents that you are using. The agent config overrides any global options here. [Learn more](/docs/agents/#additional).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Loading models
|
## Loading models
|
||||||
|
|
||||||
When opencode starts up, it checks for models in the following priority order:
|
When opencode starts up, it checks for models in the following priority order:
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ Permissions are configured in your `opencode.json` file under the `permission` k
|
|||||||
| `bash` | Control bash command execution |
|
| `bash` | Control bash command execution |
|
||||||
| `webfetch` | Control web content fetching |
|
| `webfetch` | Control web content fetching |
|
||||||
|
|
||||||
|
They can also be configured per agent, see [Agent Configuration](/docs/agents#agent-configuration) for more details.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### edit
|
### edit
|
||||||
|
|||||||
@@ -93,13 +93,9 @@ Prevent opencode from reading `.env` files:
|
|||||||
```javascript title=".opencode/plugin/env-protection.js"
|
```javascript title=".opencode/plugin/env-protection.js"
|
||||||
export const EnvProtection = async ({ client, $ }) => {
|
export const EnvProtection = async ({ client, $ }) => {
|
||||||
return {
|
return {
|
||||||
tool: {
|
"tool.execute.before": async (input, output) => {
|
||||||
execute: {
|
if (input.tool === "read" && output.args.filePath.includes(".env")) {
|
||||||
before: async (input, output) => {
|
throw new Error("Do not read .env files")
|
||||||
if (input.tool === "read" && output.args.filePath.includes(".env")) {
|
|
||||||
throw new Error("Do not read .env files")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -761,11 +761,11 @@ https://platform.openai.com/api-keys
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Zhipu AI
|
### Z.AI
|
||||||
|
|
||||||
1. Head over to the [Zhipu API console](https://z.ai/manage-apikey/apikey-list), create an account, and click **Create a new API key**.
|
1. Head over to the [Z.AI API console](https://z.ai/manage-apikey/apikey-list), create an account, and click **Create a new API key**.
|
||||||
|
|
||||||
2. Run `opencode auth login` and select **Zhipu AI**.
|
2. Run `opencode auth login` and select **Z.AI**.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ opencode auth login
|
$ opencode auth login
|
||||||
@@ -773,12 +773,12 @@ https://platform.openai.com/api-keys
|
|||||||
┌ Add credential
|
┌ Add credential
|
||||||
│
|
│
|
||||||
◆ Select provider
|
◆ Select provider
|
||||||
│ ● Zhipu AI
|
│ ● Z.AI
|
||||||
│ ...
|
│ ...
|
||||||
└
|
└
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Enter your Zhipu AI API key.
|
3. Enter your Z.AI API key.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ opencode auth login
|
$ opencode auth login
|
||||||
@@ -786,7 +786,7 @@ https://platform.openai.com/api-keys
|
|||||||
┌ Add credential
|
┌ Add credential
|
||||||
│
|
│
|
||||||
◇ Select provider
|
◇ Select provider
|
||||||
│ Zhipu AI
|
│ Z.AI
|
||||||
│
|
│
|
||||||
◇ Enter your API key
|
◇ Enter your API key
|
||||||
│ _
|
│ _
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"displayName": "opencode",
|
"displayName": "opencode",
|
||||||
"description": "opencode for VS Code",
|
"description": "opencode for VS Code",
|
||||||
"version": "0.4.26",
|
"version": "0.5.3",
|
||||||
"publisher": "sst-dev",
|
"publisher": "sst-dev",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
Reference in New Issue
Block a user