Compare commits
174
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c7bab77cb | ||
|
|
2a75aadf02 | ||
|
|
7560913b7d | ||
|
|
ba02738d34 | ||
|
|
3a416f6f33 | ||
|
|
11a37834c2 | ||
|
|
d620455531 | ||
|
|
568eccb4c6 | ||
|
|
3a07dd8d96 | ||
|
|
802ccd3788 | ||
|
|
6042785c57 | ||
|
|
5d8664c13e | ||
|
|
3d0f24067c | ||
|
|
44049540b0 | ||
|
|
40f00ccc1c | ||
|
|
a301051263 | ||
|
|
fd61be4078 | ||
|
|
4a8bdc3c75 | ||
|
|
9c7629ce61 | ||
|
|
ba53c56a21 | ||
|
|
14c0989411 | ||
|
|
36bc07a5af | ||
|
|
270b807cdf | ||
|
|
bd52ce5640 | ||
|
|
a624871ccd | ||
|
|
819d09e64e | ||
|
|
9e6cb89101 | ||
|
|
a8347c3762 | ||
|
|
57b63ea83d | ||
|
|
c162074888 | ||
|
|
088eac9d4e | ||
|
|
5fe237a3fd | ||
|
|
ae398539c5 | ||
|
|
359360ad86 | ||
|
|
5d12eb9528 | ||
|
|
6fb4f2a7a5 | ||
|
|
48dfa45a9a | ||
|
|
97520c827e | ||
|
|
b75a89776d | ||
|
|
b909679367 | ||
|
|
639d1dd8fe | ||
|
|
7033b4d0a8 | ||
|
|
87c16374aa | ||
|
|
d366a1430f | ||
|
|
cfea5c73de | ||
|
|
2589eb207f | ||
|
|
ec7c72da3f | ||
|
|
a4b36a72ad | ||
|
|
e37a9081a6 | ||
|
|
a2469d933e | ||
|
|
3cde93bf2d | ||
|
|
898bcdec87 | ||
|
|
d5971e2da5 | ||
|
|
c71f4d4847 | ||
|
|
dec7827548 | ||
|
|
7faa8cb110 | ||
|
|
d8a4a125c0 | ||
|
|
50923f06f1 | ||
|
|
ba919fb619 | ||
|
|
47b4de3531 | ||
|
|
bb6d1d502f | ||
|
|
31e964e7cf | ||
|
|
06b2304a5f | ||
|
|
1b67339e4d | ||
|
|
1571246ba8 | ||
|
|
d730d8be01 | ||
|
|
e42cc85112 | ||
|
|
c7a79f1877 | ||
|
|
431f5347af | ||
|
|
1ed4a98233 | ||
|
|
db4ff89579 | ||
|
|
2f56761060 | ||
|
|
09286ccae0 | ||
|
|
4e959849f6 | ||
|
|
3690cafeb8 | ||
|
|
bcca253dec | ||
|
|
6d69ad5574 | ||
|
|
1f9be63e96 | ||
|
|
0873908030 | ||
|
|
4db2d94854 | ||
|
|
e5d52e4eb5 | ||
|
|
f20c0bffd3 | ||
|
|
9110e6a2a7 | ||
|
|
0888c02379 | ||
|
|
24ce49d9d7 | ||
|
|
5d69f00282 | ||
|
|
12016c8eb4 | ||
|
|
d6331cf792 | ||
|
|
2d7c9c9692 | ||
|
|
1aa18c6cd6 | ||
|
|
de25703e9d | ||
|
|
1133d87be0 | ||
|
|
42aa28d512 | ||
|
|
c6bd320003 | ||
|
|
24a9841322 | ||
|
|
8bf06cbcc1 | ||
|
|
be2e6f1926 | ||
|
|
72c12d59af | ||
|
|
8408e4702e | ||
|
|
ef14f64f9e | ||
|
|
3f60a6c2a4 | ||
|
|
d447b7694a | ||
|
|
5638b782c5 | ||
|
|
eb3f337695 | ||
|
|
c88ff3c08b | ||
|
|
e0e8b94384 | ||
|
|
8f4a72c57a | ||
|
|
ef155f3766 | ||
|
|
82a323ef70 | ||
|
|
a500eaa2d4 | ||
|
|
b714bb21d2 | ||
|
|
472d01fbaf | ||
|
|
a5c15a23e4 | ||
|
|
3d189b42a3 | ||
|
|
91a3ee642d | ||
|
|
37b24f4870 | ||
|
|
38572b8175 | ||
|
|
fc1addb8f4 | ||
|
|
83b7d8e04c | ||
|
|
d27dbfe062 | ||
|
|
f8904e3972 | ||
|
|
4a5823562c | ||
|
|
3aaf29b693 | ||
|
|
6b29896a35 | ||
|
|
1bb8574179 | ||
|
|
2611c35acc | ||
|
|
00c238777a | ||
|
|
e4b548fa76 | ||
|
|
e132dd2c70 | ||
|
|
fbe9669c57 | ||
|
|
c34ad7223a | ||
|
|
cc86a64bb5 | ||
|
|
3394402aef | ||
|
|
6cd3a59022 | ||
|
|
5aeb305344 | ||
|
|
6eb043aedb | ||
|
|
e96f6385c2 | ||
|
|
1109a282e0 | ||
|
|
25f3eef957 | ||
|
|
0ca75544ab | ||
|
|
572a037e5d | ||
|
|
ad92181fa7 | ||
|
|
c56f4aa5d8 | ||
|
|
a344a766fd | ||
|
|
bca793d064 | ||
|
|
ad3c192837 | ||
|
|
5512231ca8 | ||
|
|
bad394cd49 | ||
|
|
3b97580621 | ||
|
|
cb88fe26aa | ||
|
|
e345b89ce5 | ||
|
|
26c7b240ba | ||
|
|
d327a2b1cf | ||
|
|
c1b03b728a | ||
|
|
2a2437bf22 | ||
|
|
4ccb82e81a | ||
|
|
92912219df | ||
|
|
bab3124e8b | ||
|
|
7a66ec6bc9 | ||
|
|
3a505b2691 | ||
|
|
20f43372f6 | ||
|
|
fb79dd7bf8 | ||
|
|
4025b655a4 | ||
|
|
7379903568 | ||
|
|
a685e7a805 | ||
|
|
ce7484b4f5 | ||
|
|
0bc1dcbe1b | ||
|
|
a69b339baf | ||
|
|
26f835cdd2 | ||
|
|
bd3d1413fd | ||
|
|
2c17a980ff | ||
|
|
b784c923a8 | ||
|
|
ea96f898c0 | ||
|
|
47435f6e17 |
@@ -0,0 +1,15 @@
|
|||||||
|
adamdotdevin
|
||||||
|
Brendonovich
|
||||||
|
fwang
|
||||||
|
Hona
|
||||||
|
iamdavidhill
|
||||||
|
jayair
|
||||||
|
jlongster
|
||||||
|
kitlangton
|
||||||
|
kommander
|
||||||
|
MrMushrooooom
|
||||||
|
nexxeln
|
||||||
|
R44VC0RP
|
||||||
|
rekram1-node
|
||||||
|
RhysSullivan
|
||||||
|
thdxr
|
||||||
@@ -3,12 +3,13 @@ description: "Setup Bun with caching and install dependencies"
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Mount Bun Cache
|
- name: Cache Bun dependencies
|
||||||
if: ${{ runner.os == 'Linux' }}
|
uses: actions/cache@v4
|
||||||
uses: useblacksmith/stickydisk@v1
|
|
||||||
with:
|
with:
|
||||||
key: ${{ github.repository }}-bun-cache-${{ runner.os }}
|
path: ~/.bun/install/cache
|
||||||
path: ~/.bun
|
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-bun-
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|||||||
@@ -1,7 +1,29 @@
|
|||||||
|
### Issue for this PR
|
||||||
|
|
||||||
|
Closes #
|
||||||
|
|
||||||
|
### Type of change
|
||||||
|
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature
|
||||||
|
- [ ] Refactor / code improvement
|
||||||
|
- [ ] Documentation
|
||||||
|
|
||||||
### What does this PR do?
|
### What does this PR do?
|
||||||
|
|
||||||
Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.
|
Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.
|
||||||
|
|
||||||
**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!**
|
**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!**
|
||||||
|
|
||||||
### How did you verify your code works?
|
### How did you verify your code works?
|
||||||
|
|
||||||
|
### Screenshots / recordings
|
||||||
|
|
||||||
|
_If this is a UI change, please include a screenshot or recording._
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
- [ ] I have tested my changes locally
|
||||||
|
- [ ] I have not included unrelated changes in this PR
|
||||||
|
|
||||||
|
_If you do not follow this template your PR will be automatically rejected._
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ name: duplicate-issues
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened]
|
types: [opened, edited]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-duplicates:
|
check-duplicates:
|
||||||
|
if: github.event.action == 'opened'
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -16,8 +17,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Install opencode
|
- name: Install opencode
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
@@ -27,91 +26,114 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
OPENCODE_PERMISSION: |
|
OPENCODE_PERMISSION: |
|
||||||
{
|
{
|
||||||
"bash": {
|
"bash": "deny",
|
||||||
"*": "deny",
|
"webfetch": "deny",
|
||||||
"gh issue*": "allow"
|
"edit": "deny",
|
||||||
},
|
"write": "deny"
|
||||||
"webfetch": "deny"
|
|
||||||
}
|
}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
run: |
|
run: |
|
||||||
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:
|
ISSUE_TITLE=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json title --jq .title)
|
||||||
|
ISSUE_BODY=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json body --jq .body)
|
||||||
|
|
||||||
Issue number: ${{ github.event.issue.number }}
|
PROMPT=$(cat <<EOF
|
||||||
|
Check this new issue for compliance and duplicates:
|
||||||
|
|
||||||
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
CURRENT_ISSUE_NUMBER: $ISSUE_NUMBER
|
||||||
|
|
||||||
You have TWO tasks. Perform both, then post a SINGLE comment (if needed).
|
Title: $ISSUE_TITLE
|
||||||
|
|
||||||
---
|
Description:
|
||||||
|
$ISSUE_BODY
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
TASK 1: CONTRIBUTING GUIDELINES COMPLIANCE CHECK
|
COMMENT=$(opencode run --agent duplicate-issue "$PROMPT")
|
||||||
|
|
||||||
Check whether the issue follows our contributing guidelines and issue templates.
|
if [ "$COMMENT" = "No action required" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
This project has three issue templates that every issue MUST use one of:
|
BODY="_The following comment was made by an LLM, it may be inaccurate:_
|
||||||
|
|
||||||
1. Bug Report - requires a Description field with real content
|
$COMMENT"
|
||||||
2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]:
|
|
||||||
3. Question - requires the Question field with real content
|
|
||||||
|
|
||||||
Additionally check:
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$BODY"
|
||||||
- No AI-generated walls of text (long, AI-generated descriptions are not acceptable)
|
|
||||||
- The issue has real content, not just template placeholder text left unchanged
|
|
||||||
- Bug reports should include some context about how to reproduce
|
|
||||||
- Feature requests should explain the problem or need
|
|
||||||
- We want to push for having the user provide system description & information
|
|
||||||
|
|
||||||
Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content.
|
if [[ "$COMMENT" == *"<!-- issue-compliance -->"* ]]; then
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label needs:compliance
|
||||||
|
fi
|
||||||
|
|
||||||
---
|
recheck-compliance:
|
||||||
|
if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance')
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
TASK 2: DUPLICATE CHECK
|
- name: Install opencode
|
||||||
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
Search through existing issues (excluding #${{ github.event.issue.number }}) to find potential duplicates.
|
- name: Recheck compliance
|
||||||
Consider:
|
env:
|
||||||
1. Similar titles or descriptions
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
2. Same error messages or symptoms
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
3. Related functionality or components
|
OPENCODE_PERMISSION: |
|
||||||
4. Similar feature requests
|
{
|
||||||
|
"bash": "deny",
|
||||||
|
"webfetch": "deny",
|
||||||
|
"edit": "deny",
|
||||||
|
"write": "deny"
|
||||||
|
}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
ISSUE_TITLE=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json title --jq .title)
|
||||||
|
ISSUE_BODY=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json body --jq .body)
|
||||||
|
|
||||||
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, note the pinned keybinds issue #4997.
|
PROMPT=$(cat <<EOF
|
||||||
|
Recheck this edited issue for compliance:
|
||||||
|
|
||||||
---
|
MODE: recheck-compliance
|
||||||
|
CURRENT_ISSUE_NUMBER: $ISSUE_NUMBER
|
||||||
|
|
||||||
POSTING YOUR COMMENT:
|
Title: $ISSUE_TITLE
|
||||||
|
|
||||||
Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows:
|
Description:
|
||||||
|
$ISSUE_BODY
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
If the issue is NOT compliant, start the comment with:
|
COMMENT=$(opencode run --agent duplicate-issue "$PROMPT")
|
||||||
<!-- issue-compliance -->
|
|
||||||
Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
|
|
||||||
|
|
||||||
If duplicates were found, include a section about potential duplicates with links.
|
if [ "$COMMENT" = "No action required" ]; then
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --remove-label needs:compliance || true
|
||||||
|
|
||||||
If the issue mentions keybinds/keyboard shortcuts, include a note about #4997.
|
IDS=$(gh api "repos/$REPO/issues/$ISSUE_NUMBER/comments" --jq '.[] | select(.body | contains("<!-- issue-compliance -->")) | .id')
|
||||||
|
for id in $IDS; do
|
||||||
|
gh api -X DELETE "repos/$REPO/issues/comments/$id"
|
||||||
|
done
|
||||||
|
|
||||||
If the issue IS compliant AND no duplicates were found AND no keybind reference, do NOT comment at all.
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "Thanks for updating your issue. It now meets our contributing guidelines. :+1:"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
Use this format for the comment:
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label needs:compliance
|
||||||
|
|
||||||
[If not compliant:]
|
BODY="_The following comment was made by an LLM, it may be inaccurate:_
|
||||||
<!-- issue-compliance -->
|
|
||||||
This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md).
|
|
||||||
|
|
||||||
**What needs to be fixed:**
|
$COMMENT"
|
||||||
- [specific reasons]
|
|
||||||
|
|
||||||
Please edit this issue to address the above within **2 hours**, or it will be automatically closed.
|
EXISTING=$(gh api "repos/$REPO/issues/$ISSUE_NUMBER/comments" --jq '[.[] | select(.body | contains("<!-- issue-compliance -->")) | .id] | last // empty')
|
||||||
|
if [ -n "$EXISTING" ]; then
|
||||||
|
gh api -X PATCH "repos/$REPO/issues/comments/$EXISTING" -f body="$BODY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
[If duplicates found, add:]
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$BODY"
|
||||||
---
|
|
||||||
This issue might be a duplicate of existing issues. Please check:
|
|
||||||
- #[issue_number]: [brief description of similarity]
|
|
||||||
|
|
||||||
[If keybind-related, add:]
|
|
||||||
For keybind-related issues, please also check our pinned keybinds documentation: #4997
|
|
||||||
|
|
||||||
[End with if not compliant:]
|
|
||||||
If you believe this was flagged incorrectly, please let a maintainer know.
|
|
||||||
|
|
||||||
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
name: nix-desktop
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [dev]
|
|
||||||
paths:
|
|
||||||
- "flake.nix"
|
|
||||||
- "flake.lock"
|
|
||||||
- "nix/**"
|
|
||||||
- "packages/app/**"
|
|
||||||
- "packages/desktop/**"
|
|
||||||
- ".github/workflows/nix-desktop.yml"
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "flake.nix"
|
|
||||||
- "flake.lock"
|
|
||||||
- "nix/**"
|
|
||||||
- "packages/app/**"
|
|
||||||
- "packages/desktop/**"
|
|
||||||
- ".github/workflows/nix-desktop.yml"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nix-desktop:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- blacksmith-4vcpu-ubuntu-2404
|
|
||||||
- blacksmith-4vcpu-ubuntu-2404-arm
|
|
||||||
- macos-15-intel
|
|
||||||
- macos-latest
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Nix
|
|
||||||
uses: nixbuild/nix-quick-install-action@v34
|
|
||||||
|
|
||||||
- name: Build desktop via flake
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
nix --version
|
|
||||||
nix build .#desktop -L
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
name: nix-eval
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev]
|
||||||
|
pull_request:
|
||||||
|
branches: [dev]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
nix-eval:
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Nix
|
||||||
|
uses: nixbuild/nix-quick-install-action@v34
|
||||||
|
|
||||||
|
- name: Evaluate flake outputs (all systems)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
nix --version
|
||||||
|
|
||||||
|
echo "=== Flake metadata ==="
|
||||||
|
nix flake metadata
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Flake structure ==="
|
||||||
|
nix flake show --all-systems
|
||||||
|
|
||||||
|
SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
|
||||||
|
PACKAGES="opencode"
|
||||||
|
# TODO: move 'desktop' to PACKAGES when #11755 is fixed
|
||||||
|
OPTIONAL_PACKAGES="desktop"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Evaluating packages for all systems ==="
|
||||||
|
for system in $SYSTEMS; do
|
||||||
|
echo ""
|
||||||
|
echo "--- $system ---"
|
||||||
|
for pkg in $PACKAGES; do
|
||||||
|
printf " %s: " "$pkg"
|
||||||
|
if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then
|
||||||
|
echo "✓"
|
||||||
|
else
|
||||||
|
echo "✗"
|
||||||
|
echo "::error::Evaluation failed for packages.$system.$pkg"
|
||||||
|
echo "$output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Evaluating optional packages ==="
|
||||||
|
for system in $SYSTEMS; do
|
||||||
|
echo ""
|
||||||
|
echo "--- $system ---"
|
||||||
|
for pkg in $OPTIONAL_PACKAGES; do
|
||||||
|
printf " %s: " "$pkg"
|
||||||
|
if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then
|
||||||
|
echo "✓"
|
||||||
|
else
|
||||||
|
echo "✗"
|
||||||
|
echo "::warning::Evaluation failed for packages.$system.$pkg"
|
||||||
|
echo "$output"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Evaluating devShells for all systems ==="
|
||||||
|
for system in $SYSTEMS; do
|
||||||
|
printf "%s: " "$system"
|
||||||
|
if output=$(nix eval ".#devShells.$system.default.drvPath" --raw 2>&1); then
|
||||||
|
echo "✓"
|
||||||
|
else
|
||||||
|
echo "✗"
|
||||||
|
echo "::error::Evaluation failed for devShells.$system.default"
|
||||||
|
echo "$output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== All evaluations passed ==="
|
||||||
@@ -6,7 +6,7 @@ permissions:
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [dev]
|
branches: [dev, beta]
|
||||||
paths:
|
paths:
|
||||||
- "bun.lock"
|
- "bun.lock"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
|
|||||||
@@ -6,17 +6,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-duplicates:
|
check-duplicates:
|
||||||
if: |
|
|
||||||
github.event.pull_request.user.login != 'actions-user' &&
|
|
||||||
github.event.pull_request.user.login != 'opencode' &&
|
|
||||||
github.event.pull_request.user.login != 'rekram1-node' &&
|
|
||||||
github.event.pull_request.user.login != 'thdxr' &&
|
|
||||||
github.event.pull_request.user.login != 'kommander' &&
|
|
||||||
github.event.pull_request.user.login != 'jayair' &&
|
|
||||||
github.event.pull_request.user.login != 'fwang' &&
|
|
||||||
github.event.pull_request.user.login != 'adamdotdevin' &&
|
|
||||||
github.event.pull_request.user.login != 'iamdavidhill' &&
|
|
||||||
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -27,16 +16,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Check team membership
|
||||||
|
id: team-check
|
||||||
|
run: |
|
||||||
|
LOGIN="${{ github.event.pull_request.user.login }}"
|
||||||
|
if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then
|
||||||
|
echo "is_team=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Skipping: $LOGIN is a team member or bot"
|
||||||
|
else
|
||||||
|
echo "is_team=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
|
if: steps.team-check.outputs.is_team != 'true'
|
||||||
uses: ./.github/actions/setup-bun
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
if: steps.team-check.outputs.is_team != 'true'
|
||||||
run: bun install
|
run: bun install
|
||||||
|
|
||||||
- name: Install opencode
|
- name: Install opencode
|
||||||
|
if: steps.team-check.outputs.is_team != 'true'
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
- name: Build prompt
|
- name: Build prompt
|
||||||
|
if: steps.team-check.outputs.is_team != 'true'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
@@ -53,6 +57,7 @@ jobs:
|
|||||||
} > pr_info.txt
|
} > pr_info.txt
|
||||||
|
|
||||||
- name: Check for duplicate PRs
|
- name: Check for duplicate PRs
|
||||||
|
if: steps.team-check.outputs.is_team != 'true'
|
||||||
env:
|
env:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ jobs:
|
|||||||
github.event.pull_request.user.login != 'fwang' &&
|
github.event.pull_request.user.login != 'fwang' &&
|
||||||
github.event.pull_request.user.login != 'adamdotdevin' &&
|
github.event.pull_request.user.login != 'adamdotdevin' &&
|
||||||
github.event.pull_request.user.login != 'iamdavidhill' &&
|
github.event.pull_request.user.login != 'iamdavidhill' &&
|
||||||
|
github.event.pull_request.user.login != 'R44VC0RP' &&
|
||||||
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check PR standards
|
- name: Check PR standards
|
||||||
@@ -26,6 +28,30 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const pr = context.payload.pull_request;
|
const pr = context.payload.pull_request;
|
||||||
|
const login = pr.user.login;
|
||||||
|
|
||||||
|
// Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC)
|
||||||
|
const cutoff = new Date('2026-02-19T00:00:00Z');
|
||||||
|
const prCreated = new Date(pr.created_at);
|
||||||
|
if (prCreated < cutoff) {
|
||||||
|
console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if author is a team member or bot
|
||||||
|
if (login === 'opencode-agent[bot]') return;
|
||||||
|
const { data: file } = await github.rest.repos.getContent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
path: '.github/TEAM_MEMBERS',
|
||||||
|
ref: 'dev'
|
||||||
|
});
|
||||||
|
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
|
||||||
|
if (members.includes(login)) {
|
||||||
|
console.log(`Skipping: ${login} is a team member`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const title = pr.title;
|
const title = pr.title;
|
||||||
|
|
||||||
async function addLabel(label) {
|
async function addLabel(label) {
|
||||||
@@ -137,3 +163,201 @@ jobs:
|
|||||||
|
|
||||||
await removeLabel('needs:issue');
|
await removeLabel('needs:issue');
|
||||||
console.log('PR meets all standards');
|
console.log('PR meets all standards');
|
||||||
|
|
||||||
|
check-compliance:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Check PR template compliance
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const pr = context.payload.pull_request;
|
||||||
|
const login = pr.user.login;
|
||||||
|
|
||||||
|
// Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC)
|
||||||
|
const cutoff = new Date('2026-02-19T00:00:00Z');
|
||||||
|
const prCreated = new Date(pr.created_at);
|
||||||
|
if (prCreated < cutoff) {
|
||||||
|
console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if author is a team member or bot
|
||||||
|
if (login === 'opencode-agent[bot]') return;
|
||||||
|
const { data: file } = await github.rest.repos.getContent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
path: '.github/TEAM_MEMBERS',
|
||||||
|
ref: 'dev'
|
||||||
|
});
|
||||||
|
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
|
||||||
|
if (members.includes(login)) {
|
||||||
|
console.log(`Skipping: ${login} is a team member`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = pr.body || '';
|
||||||
|
const title = pr.title;
|
||||||
|
const isDocsOrRefactor = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
|
||||||
|
|
||||||
|
const issues = [];
|
||||||
|
|
||||||
|
// Check: template sections exist
|
||||||
|
const hasWhatSection = /### What does this PR do\?/.test(body);
|
||||||
|
const hasTypeSection = /### Type of change/.test(body);
|
||||||
|
const hasVerifySection = /### How did you verify your code works\?/.test(body);
|
||||||
|
const hasChecklistSection = /### Checklist/.test(body);
|
||||||
|
const hasIssueSection = /### Issue for this PR/.test(body);
|
||||||
|
|
||||||
|
if (!hasWhatSection || !hasTypeSection || !hasVerifySection || !hasChecklistSection || !hasIssueSection) {
|
||||||
|
issues.push('PR description is missing required template sections. Please use the [PR template](../blob/dev/.github/pull_request_template.md).');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check: "What does this PR do?" has real content (not just placeholder text)
|
||||||
|
if (hasWhatSection) {
|
||||||
|
const whatMatch = body.match(/### What does this PR do\?\s*\n([\s\S]*?)(?=###|$)/);
|
||||||
|
const whatContent = whatMatch ? whatMatch[1].trim() : '';
|
||||||
|
const placeholder = 'Please provide a description of the issue';
|
||||||
|
const onlyPlaceholder = whatContent.includes(placeholder) && whatContent.replace(placeholder, '').replace(/[*\s]/g, '').length < 20;
|
||||||
|
if (!whatContent || onlyPlaceholder) {
|
||||||
|
issues.push('"What does this PR do?" section is empty or only contains placeholder text. Please describe your changes.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check: at least one "Type of change" checkbox is checked
|
||||||
|
if (hasTypeSection) {
|
||||||
|
const typeMatch = body.match(/### Type of change\s*\n([\s\S]*?)(?=###|$)/);
|
||||||
|
const typeContent = typeMatch ? typeMatch[1] : '';
|
||||||
|
const hasCheckedBox = /- \[x\]/i.test(typeContent);
|
||||||
|
if (!hasCheckedBox) {
|
||||||
|
issues.push('No "Type of change" checkbox is checked. Please select at least one.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check: issue reference (skip for docs/refactor)
|
||||||
|
if (!isDocsOrRefactor && hasIssueSection) {
|
||||||
|
const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/);
|
||||||
|
const issueContent = issueMatch ? issueMatch[1].trim() : '';
|
||||||
|
const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent);
|
||||||
|
if (!hasIssueRef) {
|
||||||
|
issues.push('No issue referenced. Please add `Closes #<number>` linking to the relevant issue.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check: "How did you verify" has content
|
||||||
|
if (hasVerifySection) {
|
||||||
|
const verifyMatch = body.match(/### How did you verify your code works\?\s*\n([\s\S]*?)(?=###|$)/);
|
||||||
|
const verifyContent = verifyMatch ? verifyMatch[1].trim() : '';
|
||||||
|
if (!verifyContent) {
|
||||||
|
issues.push('"How did you verify your code works?" section is empty. Please explain how you tested.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check: checklist boxes are checked
|
||||||
|
if (hasChecklistSection) {
|
||||||
|
const checklistMatch = body.match(/### Checklist\s*\n([\s\S]*?)(?=###|$)/);
|
||||||
|
const checklistContent = checklistMatch ? checklistMatch[1] : '';
|
||||||
|
const unchecked = (checklistContent.match(/- \[ \]/g) || []).length;
|
||||||
|
const checked = (checklistContent.match(/- \[x\]/gi) || []).length;
|
||||||
|
if (checked < 2) {
|
||||||
|
issues.push('Not all checklist items are checked. Please confirm you have tested locally and have not included unrelated changes.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
async function addLabel(label) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number,
|
||||||
|
labels: [label]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeLabel(label) {
|
||||||
|
try {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number,
|
||||||
|
name: label
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasComplianceLabel = pr.labels.some(l => l.name === 'needs:compliance');
|
||||||
|
|
||||||
|
if (issues.length > 0) {
|
||||||
|
// Non-compliant
|
||||||
|
if (!hasComplianceLabel) {
|
||||||
|
await addLabel('needs:compliance');
|
||||||
|
}
|
||||||
|
|
||||||
|
const marker = '<!-- issue-compliance -->';
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number
|
||||||
|
});
|
||||||
|
const existing = comments.find(c => c.body.includes(marker));
|
||||||
|
|
||||||
|
const body_text = `${marker}
|
||||||
|
This PR doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) and [PR template](../blob/dev/.github/pull_request_template.md).
|
||||||
|
|
||||||
|
**What needs to be fixed:**
|
||||||
|
${issues.map(i => `- ${i}`).join('\n')}
|
||||||
|
|
||||||
|
Please edit this PR description to address the above within **2 hours**, or it will be automatically closed.
|
||||||
|
|
||||||
|
If you believe this was flagged incorrectly, please let a maintainer know.`;
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: existing.id,
|
||||||
|
body: body_text
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number,
|
||||||
|
body: body_text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`PR #${pr.number} is non-compliant: ${issues.join(', ')}`);
|
||||||
|
} else if (hasComplianceLabel) {
|
||||||
|
// Was non-compliant, now fixed
|
||||||
|
await removeLabel('needs:compliance');
|
||||||
|
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number
|
||||||
|
});
|
||||||
|
const marker = '<!-- issue-compliance -->';
|
||||||
|
const existing = comments.find(c => c.body.includes(marker));
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: existing.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number,
|
||||||
|
body: 'Thanks for updating your PR! It now meets our contributing guidelines. :+1:'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`PR #${pr.number} is now compliant, label removed`);
|
||||||
|
} else {
|
||||||
|
console.log(`PR #${pr.number} is compliant`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
mode: primary
|
||||||
|
hidden: true
|
||||||
|
model: opencode/claude-haiku-4-5
|
||||||
|
color: "#E67E22"
|
||||||
|
tools:
|
||||||
|
"*": false
|
||||||
|
"github-issue-search": true
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a duplicate issue detection agent. When an issue is opened, your job is to search for potentially duplicate or related open issues.
|
||||||
|
|
||||||
|
You have two jobs:
|
||||||
|
|
||||||
|
1. Check if the issue follows our issue templates/contributing requirements.
|
||||||
|
2. Check for potential duplicate issues.
|
||||||
|
|
||||||
|
Use the github-issue-search tool to find potentially related issues.
|
||||||
|
|
||||||
|
IMPORTANT: The input will contain a line `CURRENT_ISSUE_NUMBER: NNNN`. Never mark that issue as a duplicate of itself.
|
||||||
|
|
||||||
|
The input may also contain `MODE: recheck-compliance`.
|
||||||
|
|
||||||
|
- When MODE is `recheck-compliance`, ONLY run compliance checks. Do not run duplicate checks. Do not include duplicate or keybind sections.
|
||||||
|
- When MODE is missing, do the full opened-issue behavior (compliance + duplicates + keybind note).
|
||||||
|
|
||||||
|
## Compliance checks
|
||||||
|
|
||||||
|
This project has three issue templates:
|
||||||
|
|
||||||
|
1. Bug Report - needs a Description field with real content.
|
||||||
|
2. Feature Request - title should start with `[FEATURE]:` and include verification checkbox + meaningful description.
|
||||||
|
3. Question - needs a Question field with real content.
|
||||||
|
|
||||||
|
Also check:
|
||||||
|
|
||||||
|
- no AI-generated walls of text
|
||||||
|
- required sections are not placeholder-only / unchanged template text
|
||||||
|
- bug reports include some repro context
|
||||||
|
- feature requests explain the problem/need
|
||||||
|
- encourage system information where relevant
|
||||||
|
|
||||||
|
Do not be nitpicky about optional fields. Only flag real issues (missing template/required content, placeholder-only content, obviously AI-generated wall of text, empty/nonsensical issue).
|
||||||
|
|
||||||
|
## Duplicate checks
|
||||||
|
|
||||||
|
Search for duplicates by trying multiple keyword combinations from the issue title/body. Prioritize:
|
||||||
|
|
||||||
|
- similar title/description
|
||||||
|
- same error/symptoms
|
||||||
|
- same component/feature area
|
||||||
|
|
||||||
|
If the issue mentions keybinds, keyboard shortcuts, or key bindings, include a note to check pinned issue #4997.
|
||||||
|
|
||||||
|
## Output rules
|
||||||
|
|
||||||
|
If MODE is `recheck-compliance` and the issue is compliant, output exactly:
|
||||||
|
|
||||||
|
No action required
|
||||||
|
|
||||||
|
If MODE is missing and the issue is compliant AND no duplicates are found AND no keybind note is needed, output exactly:
|
||||||
|
|
||||||
|
No action required
|
||||||
|
|
||||||
|
Otherwise output exactly one markdown comment body with this structure:
|
||||||
|
|
||||||
|
- In `recheck-compliance` mode: include only the non-compliant section and ending note.
|
||||||
|
- In default mode: include sections as needed (non-compliant, duplicates, keybind).
|
||||||
|
|
||||||
|
- If non-compliant, start with:
|
||||||
|
|
||||||
|
<!-- issue-compliance -->
|
||||||
|
|
||||||
|
This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
**What needs to be fixed:**
|
||||||
|
|
||||||
|
- [specific reason]
|
||||||
|
|
||||||
|
Please edit this issue to address the above within **2 hours**, or it will be automatically closed.
|
||||||
|
|
||||||
|
- If duplicates were found, add:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This issue might be a duplicate of existing issues. Please check:
|
||||||
|
|
||||||
|
- #1234: [brief reason]
|
||||||
|
|
||||||
|
- If keybind-related, add:
|
||||||
|
|
||||||
|
For keybind-related issues, please also check our pinned keybinds documentation: #4997
|
||||||
|
|
||||||
|
- If non-compliant, end with:
|
||||||
|
|
||||||
|
If you believe this was flagged incorrectly, please let a maintainer know.
|
||||||
|
|
||||||
|
Keep output concise. Do not wrap output in code fences.
|
||||||
@@ -69,6 +69,10 @@ Examples:
|
|||||||
- Provider integration issues
|
- Provider integration issues
|
||||||
- New, broken, or poor-quality models
|
- New, broken, or poor-quality models
|
||||||
|
|
||||||
|
#### acp
|
||||||
|
|
||||||
|
If the issue mentions acp support, assign acp label.
|
||||||
|
|
||||||
#### docs
|
#### docs
|
||||||
|
|
||||||
Add if the issue requests better documentation or docs updates.
|
Add if the issue requests better documentation or docs updates.
|
||||||
@@ -130,3 +134,7 @@ Determinism rules:
|
|||||||
- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner
|
- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner
|
||||||
|
|
||||||
In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
||||||
|
|
||||||
|
ACP:
|
||||||
|
|
||||||
|
- rekram1-node (assign any acp issues to rekram1-node)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/// <reference path="../env.d.ts" />
|
||||||
|
import { tool } from "@opencode-ai/plugin"
|
||||||
|
import DESCRIPTION from "./github-issue-search.txt"
|
||||||
|
|
||||||
|
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||||
|
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Issue {
|
||||||
|
title: string
|
||||||
|
html_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tool({
|
||||||
|
description: DESCRIPTION,
|
||||||
|
args: {
|
||||||
|
query: tool.schema.string().describe("Search query for issue titles and descriptions"),
|
||||||
|
limit: tool.schema.number().describe("Maximum number of results to return").default(10),
|
||||||
|
offset: tool.schema.number().describe("Number of results to skip for pagination").default(0),
|
||||||
|
},
|
||||||
|
async execute(args) {
|
||||||
|
const owner = "anomalyco"
|
||||||
|
const repo = "opencode"
|
||||||
|
|
||||||
|
const page = Math.floor(args.offset / args.limit) + 1
|
||||||
|
const searchQuery = encodeURIComponent(`${args.query} repo:${owner}/${repo} type:issue state:open`)
|
||||||
|
const result = await githubFetch(
|
||||||
|
`/search/issues?q=${searchQuery}&per_page=${args.limit}&page=${page}&sort=updated&order=desc`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.total_count === 0) {
|
||||||
|
return `No issues found matching "${args.query}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const issues = result.items as Issue[]
|
||||||
|
|
||||||
|
if (issues.length === 0) {
|
||||||
|
return `No other issues found matching "${args.query}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = issues.map((issue) => `${issue.title}\n${issue.html_url}`).join("\n\n")
|
||||||
|
|
||||||
|
return `Found ${result.total_count} issues (showing ${issues.length}):\n\n${formatted}`
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Use this tool to search GitHub issues by title and description.
|
||||||
|
|
||||||
|
This tool searches issues in the sst/opencode repository and returns LLM-friendly results including:
|
||||||
|
- Issue number and title
|
||||||
|
- Author
|
||||||
|
- State (open/closed/merged)
|
||||||
|
- Labels
|
||||||
|
- Description snippet
|
||||||
|
|
||||||
|
Use the query parameter to search for keywords that might appear in issue titles or descriptions.
|
||||||
@@ -68,17 +68,7 @@ export default tool({
|
|||||||
results.push("Dropped label: nix (issue does not mention nix)")
|
results.push("Dropped label: nix (issue does not mention nix)")
|
||||||
}
|
}
|
||||||
|
|
||||||
const assignee = nix
|
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
|
||||||
? "rekram1-node"
|
|
||||||
: web
|
|
||||||
? pick(TEAM.desktop)
|
|
||||||
: args.assignee === "jlongster"
|
|
||||||
? "thdxr"
|
|
||||||
: args.assignee
|
|
||||||
|
|
||||||
if (args.assignee === "jlongster" && assignee === "thdxr") {
|
|
||||||
results.push("Remapped assignee: jlongster -> thdxr (jlongster not assignable yet)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labels.includes("zen") && !zen) {
|
if (labels.includes("zen") && !zen) {
|
||||||
throw new Error("Only add the zen label when issue title/body contains 'zen'")
|
throw new Error("Only add the zen label when issue title/body contains 'zen'")
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"format_on_save": "on",
|
||||||
|
"formatter": {
|
||||||
|
"external": {
|
||||||
|
"command": "bunx",
|
||||||
|
"arguments": ["prettier", "--stdin-filepath", "{buffer_path}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,11 @@ If you are unsure if a PR would be accepted, feel free to ask a maintainer or lo
|
|||||||
|
|
||||||
Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on.
|
Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on.
|
||||||
|
|
||||||
|
## Adding New Providers
|
||||||
|
|
||||||
|
New providers shouldn't require many if ANY code changes, but if you want to add support for a new provider first make a PR to:
|
||||||
|
https://github.com/anomalyco/models.dev
|
||||||
|
|
||||||
## Developing OpenCode
|
## Developing OpenCode
|
||||||
|
|
||||||
- Requirements: Bun 1.3+
|
- Requirements: Bun 1.3+
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# Security
|
# Security
|
||||||
|
|
||||||
|
## IMPORTANT
|
||||||
|
|
||||||
|
We do not accept AI generated security reports. We receive a large number of
|
||||||
|
these and we absolutely do not have the resources to review them all. If you
|
||||||
|
submit one that will be an automatic ban from the project.
|
||||||
|
|
||||||
## Threat Model
|
## Threat Model
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
|
|||||||
@@ -14,10 +14,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/artifact": "5.0.1",
|
"@actions/artifact": "5.0.1",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
|
"@types/mime-types": "3.0.1",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "3.17.23",
|
"sst": "3.18.10",
|
||||||
"turbo": "2.5.6",
|
"turbo": "2.5.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -288,7 +289,7 @@
|
|||||||
"@ai-sdk/vercel": "1.0.33",
|
"@ai-sdk/vercel": "1.0.33",
|
||||||
"@ai-sdk/xai": "2.0.51",
|
"@ai-sdk/xai": "2.0.51",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@gitlab/gitlab-ai-provider": "3.5.1",
|
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
@@ -324,6 +325,7 @@
|
|||||||
"hono-openapi": "catalog:",
|
"hono-openapi": "catalog:",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.6",
|
||||||
@@ -356,6 +358,7 @@
|
|||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/babel__core": "7.20.5",
|
"@types/babel__core": "7.20.5",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@types/mime-types": "3.0.1",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
@@ -989,7 +992,7 @@
|
|||||||
|
|
||||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.5.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-I8+EGdUeKmGJSjAdFobHtqpxM9Fm00w0j7NJbtln/D/XQ1SKEGoZIuqJko4v0pV2mkhGUIs7qezljH/2kbXovA=="],
|
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
|
||||||
|
|
||||||
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
||||||
|
|
||||||
@@ -1609,7 +1612,7 @@
|
|||||||
|
|
||||||
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="],
|
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="],
|
||||||
|
|
||||||
"@smithy/core": ["@smithy/core@3.23.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg=="],
|
"@smithy/core": ["@smithy/core@3.23.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-HaaH4VbGie4t0+9nY3tNBRSxVTr96wzIqexUa6C2qx3MPePAuz7lIxPxYtt1Wc//SPfJLNoZJzfdt0B6ksj2jA=="],
|
||||||
|
|
||||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="],
|
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="],
|
||||||
|
|
||||||
@@ -1639,9 +1642,9 @@
|
|||||||
|
|
||||||
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="],
|
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="],
|
||||||
|
|
||||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.14", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag=="],
|
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.16", "", { "dependencies": { "@smithy/core": "^3.23.2", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-L5GICFCSsNhbJ5JSKeWFGFy16Q2OhoBizb3X2DrxaJwXSEujVvjG9Jt386dpQn2t7jINglQl0b4K/Su69BdbMA=="],
|
||||||
|
|
||||||
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.31", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg=="],
|
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.33", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-jLqZOdJhtIL4lnA9hXnAG6GgnJlo1sD3FqsTxm9wSfjviqgWesY/TMBVnT84yr4O0Vfe0jWoXlfFbzsBVph3WA=="],
|
||||||
|
|
||||||
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="],
|
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="],
|
||||||
|
|
||||||
@@ -1665,7 +1668,7 @@
|
|||||||
|
|
||||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="],
|
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="],
|
||||||
|
|
||||||
"@smithy/smithy-client": ["@smithy/smithy-client@4.11.3", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg=="],
|
"@smithy/smithy-client": ["@smithy/smithy-client@4.11.5", "", { "dependencies": { "@smithy/core": "^3.23.2", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-xixwBRqoeP2IUgcAl3U9dvJXc+qJum4lzo3maaJxifsZxKUYLfVfCXvhT4/jD01sRrHg5zjd1cw2Zmjr4/SuKQ=="],
|
||||||
|
|
||||||
"@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="],
|
"@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="],
|
||||||
|
|
||||||
@@ -1681,9 +1684,9 @@
|
|||||||
|
|
||||||
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
|
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
|
||||||
|
|
||||||
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.30", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng=="],
|
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.32", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-092sjYfFMQ/iaPH798LY/OJFBcYu0sSK34Oy9vdixhsU36zlZu8OcYjF3TD4e2ARupyK7xaxPXl+T0VIJTEkkg=="],
|
||||||
|
|
||||||
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.33", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA=="],
|
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.35", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-miz/ggz87M8VuM29y7jJZMYkn7+IErM5p5UgKIf8OtqVs/h2bXr1Bt3uTsREsI/4nK8a0PQERbAPsVPVNIsG7Q=="],
|
||||||
|
|
||||||
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="],
|
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="],
|
||||||
|
|
||||||
@@ -1917,6 +1920,8 @@
|
|||||||
|
|
||||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||||
|
|
||||||
|
"@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="],
|
||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="],
|
"@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="],
|
||||||
@@ -1983,7 +1988,7 @@
|
|||||||
|
|
||||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="],
|
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="],
|
||||||
|
|
||||||
"@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="],
|
"@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
|
||||||
|
|
||||||
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="],
|
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="],
|
||||||
|
|
||||||
@@ -2049,7 +2054,7 @@
|
|||||||
|
|
||||||
"ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="],
|
"ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="],
|
||||||
|
|
||||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||||
|
|
||||||
@@ -2131,11 +2136,11 @@
|
|||||||
|
|
||||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
"b4a": ["b4a@1.7.4", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug=="],
|
"b4a": ["b4a@1.7.5", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA=="],
|
||||||
|
|
||||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
||||||
|
|
||||||
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.3", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w=="],
|
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.5", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg=="],
|
||||||
|
|
||||||
"babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
|
"babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
|
||||||
|
|
||||||
@@ -2211,13 +2216,13 @@
|
|||||||
|
|
||||||
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
||||||
|
|
||||||
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eDgLN9teKTfmvrCqgwwmWNsNszxYs7IZdCqk0S1DCarvMhr4wcajoSBlA/nQA0/owwLduPTS8xxCnQp4/N/gDg=="],
|
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qM7W5IaFpWYGPDcNiQ8DOng3noQ97gxpH2MFH1mGsdKwI0T4oy++egSh5Z7s6AQx8WKgc9GzAsTUM4KZkFdacw=="],
|
||||||
|
|
||||||
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-X+PjwJUWenUmdQBP8EtdItMyieQ6Nlpn+BH518oaouDiSnWj5+b0Y7DNDZJq7Ezom4EaxmqL/uGYZK3aCQ7CXg=="],
|
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-oVoIsme27pcXB68YxnQSAgdNGCa4A3PGWYIBUewOh9VnJaoik4JenGb5Yy+svGE+ETFhQXV9nhHqgMPsDRrO6A=="],
|
||||||
|
|
||||||
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zMLs2YIGB+/jxrYFXaFhVKX/GBt05UTF45lc9srcHc9JXGjEj+12CIo1CHLTAWatXMTqt0Jsu6ukWEoWVT/ayA=="],
|
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+SYt09k+xDEl/GfcU7L1zdNgm7IlvAFKV5Xl/auBwuprKG5UwXNhjRlRAWfhTMCUZWN+NDf8E+ZQx0cQi9K2/g=="],
|
||||||
|
|
||||||
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Z5yAK28xrcm8Wb5k7TZ8FJKpOI/r+aVCRdlHYAqI2SDJFN3nD4mJs900X6kNVmG/xFzb5yOuKVYWGg+6ZXWbyA=="],
|
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-zvnUl4EAsQbKsmZVu+lEJcH8axQ7MiCfqg2OmnHd6uw1THABmHaX0GbpKiHshdgadNN2Nf+4zDyTJB5YMcAdrA=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
@@ -2237,7 +2242,7 @@
|
|||||||
|
|
||||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="],
|
||||||
|
|
||||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||||
|
|
||||||
@@ -2471,7 +2476,7 @@
|
|||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||||
|
|
||||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
"error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="],
|
"error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="],
|
||||||
|
|
||||||
@@ -2707,7 +2712,7 @@
|
|||||||
|
|
||||||
"h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="],
|
"h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="],
|
||||||
|
|
||||||
"happy-dom": ["happy-dom@20.6.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^6.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-+0vhESXXhFwkdjZnJ5DlmJIfUYGgIEEjzIjB+aKJbFuqlvvKyOi+XkI1fYbgYR9QCxG5T08koxsQ6HrQfa5gCQ=="],
|
"happy-dom": ["happy-dom@20.6.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Xk/Y0cuq9ngN/my8uvK4gKoyDl6sBKkIl8A/hJ0IabZVH7E5SJLHNE7uKRPVmSrQbhJaLIHTEcvTct4GgNtsRA=="],
|
||||||
|
|
||||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||||
|
|
||||||
@@ -2919,7 +2924,7 @@
|
|||||||
|
|
||||||
"is-whitespace": ["is-whitespace@0.3.0", "", {}, "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg=="],
|
"is-whitespace": ["is-whitespace@0.3.0", "", {}, "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg=="],
|
||||||
|
|
||||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||||
|
|
||||||
"is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="],
|
"is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="],
|
||||||
|
|
||||||
@@ -3497,7 +3502,7 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="],
|
"punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="],
|
||||||
|
|
||||||
"qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
|
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||||
|
|
||||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||||
|
|
||||||
@@ -3759,23 +3764,23 @@
|
|||||||
|
|
||||||
"srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="],
|
"srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="],
|
||||||
|
|
||||||
"sst": ["sst@3.17.23", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.23", "sst-darwin-x64": "3.17.23", "sst-linux-arm64": "3.17.23", "sst-linux-x64": "3.17.23", "sst-linux-x86": "3.17.23", "sst-win32-arm64": "3.17.23", "sst-win32-x64": "3.17.23", "sst-win32-x86": "3.17.23" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-TwKgUgDnZdc1Swe+bvCNeyO4dQnYz5cTodMpYj3jlXZdK9/KNz0PVxT1f0u5E76i1pmilXrUBL/f7iiMPw4RDg=="],
|
"sst": ["sst@3.18.10", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.18.10", "sst-darwin-x64": "3.18.10", "sst-linux-arm64": "3.18.10", "sst-linux-x64": "3.18.10", "sst-linux-x86": "3.18.10", "sst-win32-arm64": "3.18.10", "sst-win32-x64": "3.18.10", "sst-win32-x86": "3.18.10" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-SY+ldeJ9K5E9q+DhjXA3e2W3BEOzBwkE3IyLSD71uA3/5nRhUAST31iOWEpW36LbIvSQ9uOVDFcebztoLJ8s7w=="],
|
||||||
|
|
||||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R6kvmF+rUideOoU7KBs2SdvrIupoE+b+Dor/eq9Uo4Dojj7KvYDZI/EDm8sSCbbcx/opiWeyNqKtlnLEdCxE6g=="],
|
"sst-darwin-arm64": ["sst-darwin-arm64@3.18.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3MwIpMZhhdZKDqLp9ZQNlwkWix5+q+N0PWstuTomYwgZOxCCe6u9IIsoIszSk+GAJJN/jvGZyLiXKeV4iiQvw=="],
|
||||||
|
|
||||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-WW4P1S35iYCifQXxD+sE3wuzcN+LHLpuKMaNoaBqEcWGZnH3IPaDJ7rpLF0arkDAo/z3jZmWWzOCkr0JuqJ8vQ=="],
|
"sst-darwin-x64": ["sst-darwin-x64@3.18.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-nQ0jMKkPOa+kj6Ygz8+kYhBua/vgNTLkd+4r8NSmk7v+Zs78lKnx3T//kEzS0yik6Q6QwGfokwrTcA1Jii2xSw=="],
|
||||||
|
|
||||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-TjtNqgIh7RlAWgPLFCAt0mXvIB+J7WjmRvIRrAdX0mXsndOiBJ/DMOgXSLVsIWHCfPj8MIEot/hWpnJgXgIeag=="],
|
"sst-linux-arm64": ["sst-linux-arm64@3.18.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-mj9VNj3SvLS+HaXx2PhCX0aTA7CwJNoM6JhRc0s/zCilqchcvqDjbhpYBJO4brEPv6aOaaa7T3WvIQqtYauK4Q=="],
|
||||||
|
|
||||||
"sst-linux-x64": ["sst-linux-x64@3.17.23", "", { "os": "linux", "cpu": "x64" }, "sha512-qdqJiEbYfCjZlI3F/TA6eoIU7JXVkEEI/UMILNf2JWhky0KQdCW2Xyz+wb6c0msVJCWdUM/uj+1DaiP2eXvghw=="],
|
"sst-linux-x64": ["sst-linux-x64@3.18.10", "", { "os": "linux", "cpu": "x64" }, "sha512-7iy1Eq2eqnT9Ag/8OVgC04vRjV7AAQyf/BvzLc+6Sz+GvRiKA8VEuPnbXNYQF+NIvEqsawfcd7MknSTtImpsvQ=="],
|
||||||
|
|
||||||
"sst-linux-x86": ["sst-linux-x86@3.17.23", "", { "os": "linux", "cpu": "none" }, "sha512-aGmUujIvoNlmAABEGsOgfY1rxD9koC6hN8bnTLbDI+oI/u/zjHYh50jsbL0p3TlaHpwF/lxP3xFSuT6IKp+KgA=="],
|
"sst-linux-x86": ["sst-linux-x86@3.18.10", "", { "os": "linux", "cpu": "none" }, "sha512-77qZSuPZeQ5bdRCiq1pQEdY8EcGNHboKrx4P2yFid2FBDKJsXxOXtIxJdloyx+ljBn0+nxl/g040QBmXxdc9tA=="],
|
||||||
|
|
||||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZxdkGqYDrrZGz98rijDCN+m5yuCcwD6Bc9/6hubLsvdpNlVorUqzpg801Ec97xSK0nIC9g6pNiRyxAcsQQstUg=="],
|
"sst-win32-arm64": ["sst-win32-arm64@3.18.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aY+FhMxvYs8crlrKALpLn/kKmud8YQj6LkMHsrOAAIJhfNyxhCja2vrYQaY+bcqdsS5W2LMVcS2hyaMqKXZKcg=="],
|
||||||
|
|
||||||
"sst-win32-x64": ["sst-win32-x64@3.17.23", "", { "os": "win32", "cpu": "x64" }, "sha512-yc9cor4MS49Ccy2tQCF1tf6M81yLeSGzGL+gjhUxpVKo2pN3bxl3w70eyU/mTXSEeyAmG9zEfbt6FNu4sy5cUA=="],
|
"sst-win32-x64": ["sst-win32-x64@3.18.10", "", { "os": "win32", "cpu": "x64" }, "sha512-rY+yJXOpG+P5xXnaQRpCvBK2zwwLhjzpYidGkp6F+cGgiVdh2Wre/CIQNRaVHr20ncj8lLe/RsHWa9QCNM48jg=="],
|
||||||
|
|
||||||
"sst-win32-x86": ["sst-win32-x86@3.17.23", "", { "os": "win32", "cpu": "none" }, "sha512-DIp3s54IpNAfdYjSRt6McvkbEPQDMxUu6RUeRAd2C+FcTJgTloon/ghAPQBaDgu2VoVgymjcJARO/XyfKcCLOQ=="],
|
"sst-win32-x86": ["sst-win32-x86@3.18.10", "", { "os": "win32", "cpu": "none" }, "sha512-pq8SmV0pIjBFMY6DraUZ4akyTxHnfjIKCRbBLdMxFUZK8TzA1NK2YdjRt1AwrgXRYGRyctrz/mt4WyO0SMOVQQ=="],
|
||||||
|
|
||||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||||
|
|
||||||
@@ -3841,19 +3846,19 @@
|
|||||||
|
|
||||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
"tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="],
|
"tar": ["tar@7.5.9", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg=="],
|
||||||
|
|
||||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||||
|
|
||||||
"tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
|
"tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
|
||||||
|
|
||||||
"tedious": ["tedious@19.2.1", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.5", "@types/node": ">=18", "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA=="],
|
"tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
|
||||||
|
|
||||||
"terracotta": ["terracotta@1.1.0", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww=="],
|
"terracotta": ["terracotta@1.1.0", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww=="],
|
||||||
|
|
||||||
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
||||||
|
|
||||||
"text-decoder": ["text-decoder@1.2.4", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-mzlffA3tBNhziEHPK5L5InZg1d/ElNIpJhnhbDRNUtem/edZcJ5zg5FgwKKKOyklxk+6Jt+TrSu83musmvrDlg=="],
|
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
|
||||||
|
|
||||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||||
|
|
||||||
@@ -3961,7 +3966,7 @@
|
|||||||
|
|
||||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||||
|
|
||||||
"undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="],
|
"undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||||
|
|
||||||
@@ -4127,7 +4132,7 @@
|
|||||||
|
|
||||||
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
|
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
|
||||||
|
|
||||||
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
||||||
|
|
||||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||||
|
|
||||||
@@ -4277,8 +4282,6 @@
|
|||||||
|
|
||||||
"@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
"@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||||
|
|
||||||
"@azure/core-http/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
|
||||||
|
|
||||||
"@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
"@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
||||||
|
|
||||||
"@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
"@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||||
@@ -4299,7 +4302,7 @@
|
|||||||
|
|
||||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.21.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-26dQFi76dB8IiN/WKGQOV+yKKTTlRCxQjoi2WLt0kMcH8pvxVyvfdBDkld5GTl7W1qvBpwVOtFcsqktj3fBRpA=="],
|
"@gitlab/gitlab-ai-provider/openai": ["openai@6.22.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
@@ -4501,7 +4504,9 @@
|
|||||||
|
|
||||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="],
|
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="],
|
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="],
|
||||||
|
|
||||||
@@ -4509,6 +4514,8 @@
|
|||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="],
|
"ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||||
|
|
||||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
@@ -4529,6 +4536,8 @@
|
|||||||
|
|
||||||
"aws-sdk/events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="],
|
"aws-sdk/events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="],
|
||||||
|
|
||||||
|
"aws-sdk/xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
||||||
|
|
||||||
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
||||||
|
|
||||||
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||||
@@ -4539,6 +4548,10 @@
|
|||||||
|
|
||||||
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||||
|
|
||||||
|
"body-parser/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
|
||||||
|
|
||||||
|
"buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
|
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
|
||||||
|
|
||||||
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||||
@@ -4573,6 +4586,8 @@
|
|||||||
|
|
||||||
"express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
"express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
||||||
|
|
||||||
|
"express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
|
||||||
|
|
||||||
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||||
|
|
||||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
@@ -4581,7 +4596,7 @@
|
|||||||
|
|
||||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||||
|
|
||||||
"glob/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="],
|
"glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="],
|
||||||
|
|
||||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
@@ -4619,8 +4634,6 @@
|
|||||||
|
|
||||||
"mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
"mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||||
|
|
||||||
"mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
|
|
||||||
|
|
||||||
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
|
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
|
||||||
|
|
||||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||||
@@ -4649,10 +4662,10 @@
|
|||||||
|
|
||||||
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||||
|
|
||||||
"parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
|
||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
|
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||||
|
|
||||||
"path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
"path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||||
|
|
||||||
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||||
@@ -4709,6 +4722,8 @@
|
|||||||
|
|
||||||
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
|
||||||
|
"tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
@@ -4725,6 +4740,8 @@
|
|||||||
|
|
||||||
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||||
|
|
||||||
|
"unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||||
|
|
||||||
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
"vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
@@ -4745,6 +4762,8 @@
|
|||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
|
||||||
|
|
||||||
"yaml-language-server/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
"yaml-language-server/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
"yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
|
"yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
|
||||||
@@ -5005,8 +5024,6 @@
|
|||||||
|
|
||||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="],
|
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="],
|
||||||
@@ -5015,7 +5032,7 @@
|
|||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
"ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||||
|
|
||||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
@@ -5035,6 +5052,8 @@
|
|||||||
|
|
||||||
"astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
"astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||||
|
|
||||||
|
"aws-sdk/xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
|
||||||
|
|
||||||
"babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
|
"babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
|
||||||
|
|
||||||
"babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
"babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||||
@@ -5069,8 +5088,6 @@
|
|||||||
|
|
||||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
"mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
|
||||||
|
|
||||||
"opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
"opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
/// <reference path="../sst-env.d.ts" />
|
/// <reference path="../sst-env.d.ts" />
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -214,9 +214,9 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
server: {
|
server: {
|
||||||
|
placement: { region: "aws:us-east-1" },
|
||||||
transform: {
|
transform: {
|
||||||
worker: {
|
worker: {
|
||||||
placement: { mode: "smart" },
|
|
||||||
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-C3WIEER2XgzO85wk2sp3BzQ6dknW026zslD8nKZjo2U=",
|
"x86_64-linux": "sha256-7y6gQyIxyrdp2DaG/0oOEpuL+1n9oa8arUn1CuDiDhA=",
|
||||||
"aarch64-linux": "sha256-+tTJHZMZ/+8fAjI/1fUTuca8J2MZfB+5vhBoZ7jgqcE=",
|
"aarch64-linux": "sha256-7dnHO2WqQZ9A8cG3EC8p7408YR9n2F5C6DG5rNWHqNY=",
|
||||||
"aarch64-darwin": "sha256-vS82puFGBBToxyIBa8Zi0KLKdJYr64T6HZL2rL32mH8=",
|
"aarch64-darwin": "sha256-jxjhnVfE61RVOHaWvDO4mGLk6guQ8jHeXv/pbu5nbaE=",
|
||||||
"x86_64-darwin": "sha256-Tr8JMTCxV6WVt3dXV7iq3PNCm2Cn+RXAbU9+o7pKKV0="
|
"x86_64-darwin": "sha256-22yM4FEtVxGWRug6H0rKog86Q/cYE3QsADrRbLeJKVQ="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -69,10 +69,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/artifact": "5.0.1",
|
"@actions/artifact": "5.0.1",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
|
"@types/mime-types": "3.0.1",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "3.17.23",
|
"sst": "3.18.10",
|
||||||
"turbo": "2.5.6"
|
"turbo": "2.5.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+37
-64
@@ -1,35 +1,36 @@
|
|||||||
import "@/index.css"
|
import "@/index.css"
|
||||||
import { ErrorBoundary, Show, Suspense, lazy, type JSX, type ParentProps } from "solid-js"
|
|
||||||
import { Router, Route, Navigate } from "@solidjs/router"
|
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
|
||||||
import { Font } from "@opencode-ai/ui/font"
|
|
||||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
|
||||||
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
|
||||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
|
||||||
import { I18nProvider } from "@opencode-ai/ui/context"
|
|
||||||
import { Diff } from "@opencode-ai/ui/diff"
|
|
||||||
import { Code } from "@opencode-ai/ui/code"
|
import { Code } from "@opencode-ai/ui/code"
|
||||||
|
import { I18nProvider } from "@opencode-ai/ui/context"
|
||||||
|
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||||
|
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||||
|
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||||
|
import { Diff } from "@opencode-ai/ui/diff"
|
||||||
|
import { Font } from "@opencode-ai/ui/font"
|
||||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||||
import { GlobalSyncProvider } from "@/context/global-sync"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import { PermissionProvider } from "@/context/permission"
|
import { Navigate, Route, Router } from "@solidjs/router"
|
||||||
import { LayoutProvider } from "@/context/layout"
|
import { ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
|
||||||
|
import { CommandProvider } from "@/context/command"
|
||||||
|
import { CommentsProvider } from "@/context/comments"
|
||||||
|
import { FileProvider } from "@/context/file"
|
||||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||||
import { normalizeServerUrl, ServerProvider, useServer } from "@/context/server"
|
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||||
|
import { HighlightsProvider } from "@/context/highlights"
|
||||||
|
import { LanguageProvider, useLanguage } from "@/context/language"
|
||||||
|
import { LayoutProvider } from "@/context/layout"
|
||||||
|
import { ModelsProvider } from "@/context/models"
|
||||||
|
import { NotificationProvider } from "@/context/notification"
|
||||||
|
import { PermissionProvider } from "@/context/permission"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { PromptProvider } from "@/context/prompt"
|
||||||
|
import { type ServerConnection, ServerProvider, useServer } from "@/context/server"
|
||||||
import { SettingsProvider } from "@/context/settings"
|
import { SettingsProvider } from "@/context/settings"
|
||||||
import { TerminalProvider } from "@/context/terminal"
|
import { TerminalProvider } from "@/context/terminal"
|
||||||
import { PromptProvider } from "@/context/prompt"
|
|
||||||
import { FileProvider } from "@/context/file"
|
|
||||||
import { CommentsProvider } from "@/context/comments"
|
|
||||||
import { NotificationProvider } from "@/context/notification"
|
|
||||||
import { ModelsProvider } from "@/context/models"
|
|
||||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { CommandProvider } from "@/context/command"
|
|
||||||
import { LanguageProvider, useLanguage } from "@/context/language"
|
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { HighlightsProvider } from "@/context/highlights"
|
|
||||||
import Layout from "@/pages/layout"
|
|
||||||
import DirectoryLayout from "@/pages/directory-layout"
|
import DirectoryLayout from "@/pages/directory-layout"
|
||||||
|
import Layout from "@/pages/layout"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
|
|
||||||
const Home = lazy(() => import("@/pages/home"))
|
const Home = lazy(() => import("@/pages/home"))
|
||||||
const Session = lazy(() => import("@/pages/session"))
|
const Session = lazy(() => import("@/pages/session"))
|
||||||
const Loading = () => <div class="size-full" />
|
const Loading = () => <div class="size-full" />
|
||||||
@@ -57,7 +58,11 @@ function UiI18nBridge(props: ParentProps) {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[]; wsl?: boolean }
|
__OPENCODE__?: {
|
||||||
|
updaterEnabled?: boolean
|
||||||
|
deepLinks?: string[]
|
||||||
|
wsl?: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,30 +112,6 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStoredDefaultServerUrl = (platform: ReturnType<typeof usePlatform>) => {
|
|
||||||
if (platform.platform !== "web") return
|
|
||||||
const result = platform.getDefaultServerUrl?.()
|
|
||||||
if (result instanceof Promise) return
|
|
||||||
if (!result) return
|
|
||||||
return normalizeServerUrl(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveDefaultServerUrl = (props: {
|
|
||||||
defaultUrl?: string
|
|
||||||
storedDefaultServerUrl?: string
|
|
||||||
hostname: string
|
|
||||||
origin: string
|
|
||||||
isDev: boolean
|
|
||||||
devHost?: string
|
|
||||||
devPort?: string
|
|
||||||
}) => {
|
|
||||||
if (props.defaultUrl) return props.defaultUrl
|
|
||||||
if (props.storedDefaultServerUrl) return props.storedDefaultServerUrl
|
|
||||||
if (props.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
|
||||||
if (props.isDev) return `http://${props.devHost ?? "localhost"}:${props.devPort ?? "4096"}`
|
|
||||||
return props.origin
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppBaseProviders(props: ParentProps) {
|
export function AppBaseProviders(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
@@ -157,27 +138,19 @@ export function AppBaseProviders(props: ParentProps) {
|
|||||||
function ServerKey(props: ParentProps) {
|
function ServerKey(props: ParentProps) {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
return (
|
return (
|
||||||
<Show when={server.url} keyed>
|
<Show when={server.key} keyed>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) {
|
export function AppInterface(props: {
|
||||||
const platform = usePlatform()
|
children?: JSX.Element
|
||||||
const storedDefaultServerUrl = getStoredDefaultServerUrl(platform)
|
defaultServer: ServerConnection.Key
|
||||||
const defaultServerUrl = resolveDefaultServerUrl({
|
servers?: Array<ServerConnection.Any>
|
||||||
defaultUrl: props.defaultUrl,
|
}) {
|
||||||
storedDefaultServerUrl,
|
|
||||||
hostname: location.hostname,
|
|
||||||
origin: window.location.origin,
|
|
||||||
isDev: import.meta.env.DEV,
|
|
||||||
devHost: import.meta.env.VITE_OPENCODE_SERVER_HOST,
|
|
||||||
devPort: import.meta.env.VITE_OPENCODE_SERVER_PORT,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServerProvider defaultUrl={defaultServerUrl} isSidecar={props.isSidecar}>
|
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
||||||
<ServerKey>
|
<ServerKey>
|
||||||
<GlobalSDKProvider>
|
<GlobalSDKProvider>
|
||||||
<GlobalSyncProvider>
|
<GlobalSyncProvider>
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
|
||||||
import { normalizeServerUrl, useServer } from "@/context/server"
|
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { useNavigate } from "@solidjs/router"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { List } from "@opencode-ai/ui/list"
|
||||||
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
import { useNavigate } from "@solidjs/router"
|
||||||
|
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
||||||
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { ServerRow } from "@/components/server/server-row"
|
import { ServerRow } from "@/components/server/server-row"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
|
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||||
|
|
||||||
interface AddRowProps {
|
interface AddRowProps {
|
||||||
@@ -89,7 +88,7 @@ function useServerPreview(fetcher: typeof fetch) {
|
|||||||
if (!looksComplete(value)) return
|
if (!looksComplete(value)) return
|
||||||
const normalized = normalizeServerUrl(value)
|
const normalized = normalizeServerUrl(value)
|
||||||
if (!normalized) return
|
if (!normalized) return
|
||||||
const result = await checkServerHealth(normalized, fetcher)
|
const result = await checkServerHealth({ url: normalized }, fetcher)
|
||||||
setStatus(result.healthy)
|
setStatus(result.healthy)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,14 +170,13 @@ export function DialogSelectServer() {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const globalSDK = useGlobalSDK()
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const fetcher = platform.fetch ?? globalThis.fetch
|
const fetcher = platform.fetch ?? globalThis.fetch
|
||||||
const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language)
|
const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language)
|
||||||
const { previewStatus } = useServerPreview(fetcher)
|
const { previewStatus } = useServerPreview(fetcher)
|
||||||
let listRoot: HTMLDivElement | undefined
|
let listRoot: HTMLDivElement | undefined
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
status: {} as Record<string, ServerHealth | undefined>,
|
status: {} as Record<ServerConnection.Key, ServerHealth | undefined>,
|
||||||
addServer: {
|
addServer: {
|
||||||
url: "",
|
url: "",
|
||||||
adding: false,
|
adding: false,
|
||||||
@@ -214,24 +212,25 @@ export function DialogSelectServer() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceServer = (original: string, next: string) => {
|
const replaceServer = (original: ServerConnection.Http, next: string) => {
|
||||||
const active = server.url
|
const active = server.key
|
||||||
const nextActive = active === original ? next : active
|
const newConn = server.add(next)
|
||||||
|
if (!newConn) return
|
||||||
|
|
||||||
server.add(next)
|
const nextActive = active === ServerConnection.key(original) ? ServerConnection.key(newConn) : active
|
||||||
if (nextActive) server.setActive(nextActive)
|
if (nextActive) server.setActive(nextActive)
|
||||||
server.remove(original)
|
server.remove(ServerConnection.key(original))
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = createMemo(() => {
|
const items = createMemo(() => {
|
||||||
const current = server.url
|
const current = server.current
|
||||||
const list = server.list
|
const list = server.list
|
||||||
if (!current) return list
|
if (!current) return list
|
||||||
if (!list.includes(current)) return [current, ...list]
|
if (!list.includes(current)) return [current, ...list]
|
||||||
return [current, ...list.filter((x) => x !== current)]
|
return [current, ...list.filter((x) => x !== current)]
|
||||||
})
|
})
|
||||||
|
|
||||||
const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0])
|
const current = createMemo(() => items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0])
|
||||||
|
|
||||||
const sortedItems = createMemo(() => {
|
const sortedItems = createMemo(() => {
|
||||||
const list = items()
|
const list = items()
|
||||||
@@ -246,17 +245,17 @@ export function DialogSelectServer() {
|
|||||||
return list.slice().sort((a, b) => {
|
return list.slice().sort((a, b) => {
|
||||||
if (a === active) return -1
|
if (a === active) return -1
|
||||||
if (b === active) return 1
|
if (b === active) return 1
|
||||||
const diff = rank(store.status[a]) - rank(store.status[b])
|
const diff = rank(store.status[ServerConnection.key(a)]) - rank(store.status[ServerConnection.key(b)])
|
||||||
if (diff !== 0) return diff
|
if (diff !== 0) return diff
|
||||||
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function refreshHealth() {
|
async function refreshHealth() {
|
||||||
const results: Record<string, ServerHealth> = {}
|
const results: Record<ServerConnection.Key, ServerHealth> = {}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
items().map(async (url) => {
|
items().map(async (conn) => {
|
||||||
results[url] = await checkServerHealth(url, fetcher)
|
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
setStore("status", reconcile(results))
|
setStore("status", reconcile(results))
|
||||||
@@ -269,15 +268,15 @@ export function DialogSelectServer() {
|
|||||||
onCleanup(() => clearInterval(interval))
|
onCleanup(() => clearInterval(interval))
|
||||||
})
|
})
|
||||||
|
|
||||||
async function select(value: string, persist?: boolean) {
|
async function select(conn: ServerConnection.Any, persist?: boolean) {
|
||||||
if (!persist && store.status[value]?.healthy === false) return
|
if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return
|
||||||
dialog.close()
|
dialog.close()
|
||||||
if (persist) {
|
if (persist) {
|
||||||
server.add(value)
|
server.add(conn.http.url)
|
||||||
navigate("/")
|
navigate("/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
server.setActive(value)
|
server.setActive(ServerConnection.key(conn))
|
||||||
navigate("/")
|
navigate("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +310,7 @@ export function DialogSelectServer() {
|
|||||||
|
|
||||||
setStore("addServer", { adding: true, error: "" })
|
setStore("addServer", { adding: true, error: "" })
|
||||||
|
|
||||||
const result = await checkServerHealth(normalized, fetcher)
|
const result = await checkServerHealth({ url: normalized }, fetcher)
|
||||||
setStore("addServer", { adding: false })
|
setStore("addServer", { adding: false })
|
||||||
|
|
||||||
if (!result.healthy) {
|
if (!result.healthy) {
|
||||||
@@ -320,25 +319,25 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetAdd()
|
resetAdd()
|
||||||
await select(normalized, true)
|
await select({ type: "http", http: { url: normalized } }, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEdit(original: string, value: string) {
|
async function handleEdit(original: ServerConnection.Any, value: string) {
|
||||||
if (store.editServer.busy) return
|
if (store.editServer.busy || original.type !== "http") return
|
||||||
const normalized = normalizeServerUrl(value)
|
const normalized = normalizeServerUrl(value)
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
resetEdit()
|
resetEdit()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized === original) {
|
if (normalized === original.http.url) {
|
||||||
resetEdit()
|
resetEdit()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setStore("editServer", { busy: true, error: "" })
|
setStore("editServer", { busy: true, error: "" })
|
||||||
|
|
||||||
const result = await checkServerHealth(normalized, fetcher)
|
const result = await checkServerHealth({ url: normalized }, fetcher)
|
||||||
setStore("editServer", { busy: false })
|
setStore("editServer", { busy: false })
|
||||||
|
|
||||||
if (!result.healthy) {
|
if (!result.healthy) {
|
||||||
@@ -366,7 +365,7 @@ export function DialogSelectServer() {
|
|||||||
handleAdd(store.addServer.url)
|
handleAdd(store.addServer.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditKey = (event: KeyboardEvent, original: string) => {
|
const handleEditKey = (event: KeyboardEvent, original: ServerConnection.Any) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -378,7 +377,7 @@ export function DialogSelectServer() {
|
|||||||
handleEdit(original, store.editServer.value)
|
handleEdit(original, store.editServer.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemove(url: string) {
|
async function handleRemove(url: ServerConnection.Key) {
|
||||||
server.remove(url)
|
server.remove(url)
|
||||||
if ((await platform.getDefaultServerUrl?.()) === url) {
|
if ((await platform.getDefaultServerUrl?.()) === url) {
|
||||||
platform.setDefaultServerUrl?.(null)
|
platform.setDefaultServerUrl?.(null)
|
||||||
@@ -390,11 +389,14 @@ export function DialogSelectServer() {
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div ref={(el) => (listRoot = el)}>
|
<div ref={(el) => (listRoot = el)}>
|
||||||
<List
|
<List
|
||||||
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: false }}
|
search={{
|
||||||
|
placeholder: language.t("dialog.server.search.placeholder"),
|
||||||
|
autofocus: false,
|
||||||
|
}}
|
||||||
noInitialSelection
|
noInitialSelection
|
||||||
emptyMessage={language.t("dialog.server.empty")}
|
emptyMessage={language.t("dialog.server.empty")}
|
||||||
items={sortedItems}
|
items={sortedItems}
|
||||||
key={(x) => x}
|
key={(x) => x.http.url}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
if (x) select(x)
|
if (x) select(x)
|
||||||
}}
|
}}
|
||||||
@@ -425,10 +427,11 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
|
const key = ServerConnection.key(i)
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
||||||
<Show
|
<Show
|
||||||
when={store.editServer.id !== i}
|
when={store.editServer.id !== i.http.url}
|
||||||
fallback={
|
fallback={
|
||||||
<EditRow
|
<EditRow
|
||||||
value={store.editServer.value}
|
value={store.editServer.value}
|
||||||
@@ -443,12 +446,12 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ServerRow
|
<ServerRow
|
||||||
url={i}
|
conn={i}
|
||||||
status={store.status[i]}
|
status={store.status[key]}
|
||||||
dimmed={store.status[i]?.healthy === false}
|
dimmed={store.status[key]?.healthy === false}
|
||||||
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
||||||
badge={
|
badge={
|
||||||
<Show when={defaultUrl() === i}>
|
<Show when={defaultUrl() === i.http.url}>
|
||||||
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||||
{language.t("dialog.server.status.default")}
|
{language.t("dialog.server.status.default")}
|
||||||
</span>
|
</span>
|
||||||
@@ -456,59 +459,63 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={store.editServer.id !== i}>
|
<Show when={store.editServer.id !== i.http.url}>
|
||||||
<div class="flex items-center justify-center gap-5 pl-4">
|
<div class="flex items-center justify-center gap-5 pl-4">
|
||||||
<Show when={current() === i}>
|
<Show when={ServerConnection.key(current()) === key}>
|
||||||
<p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
|
<p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<DropdownMenu>
|
<Show when={i.type === "http"}>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu>
|
||||||
as={IconButton}
|
<DropdownMenu.Trigger
|
||||||
icon="dot-grid"
|
as={IconButton}
|
||||||
variant="ghost"
|
icon="dot-grid"
|
||||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
variant="ghost"
|
||||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||||
/>
|
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||||
<DropdownMenu.Portal>
|
/>
|
||||||
<DropdownMenu.Content class="mt-1">
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Content class="mt-1">
|
||||||
onSelect={() => {
|
<DropdownMenu.Item
|
||||||
setStore("editServer", {
|
onSelect={() => {
|
||||||
id: i,
|
setStore("editServer", {
|
||||||
value: i,
|
id: i.http.url,
|
||||||
error: "",
|
value: i.http.url,
|
||||||
status: store.status[i]?.healthy,
|
error: "",
|
||||||
})
|
status: store.status[ServerConnection.key(i)]?.healthy,
|
||||||
}}
|
})
|
||||||
>
|
}}
|
||||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
>
|
||||||
</DropdownMenu.Item>
|
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||||
<Show when={canDefault() && defaultUrl() !== i}>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onSelect={() => setDefault(i)}>
|
<Show when={canDefault() && defaultUrl() !== i.http.url}>
|
||||||
|
<DropdownMenu.Item onSelect={() => setDefault(i.http.url)}>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("dialog.server.menu.default")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</Show>
|
||||||
|
<Show when={canDefault() && defaultUrl() === i.http.url}>
|
||||||
|
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("dialog.server.menu.defaultRemove")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</Show>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => handleRemove(ServerConnection.key(i))}
|
||||||
|
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||||
|
>
|
||||||
<DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>
|
||||||
{language.t("dialog.server.menu.default")}
|
{language.t("dialog.server.menu.delete")}
|
||||||
</DropdownMenu.ItemLabel>
|
</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Show>
|
</DropdownMenu.Content>
|
||||||
<Show when={canDefault() && defaultUrl() === i}>
|
</DropdownMenu.Portal>
|
||||||
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
</DropdownMenu>
|
||||||
<DropdownMenu.ItemLabel>
|
</Show>
|
||||||
{language.t("dialog.server.menu.defaultRemove")}
|
|
||||||
</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</Show>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => handleRemove(i)}
|
|
||||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,13 +71,13 @@ const kindLabel = (kind: Kind) => {
|
|||||||
const kindTextColor = (kind: Kind) => {
|
const kindTextColor = (kind: Kind) => {
|
||||||
if (kind === "add") return "color: var(--icon-diff-add-base)"
|
if (kind === "add") return "color: var(--icon-diff-add-base)"
|
||||||
if (kind === "del") return "color: var(--icon-diff-delete-base)"
|
if (kind === "del") return "color: var(--icon-diff-delete-base)"
|
||||||
return "color: var(--icon-warning-active)"
|
return "color: var(--icon-diff-modified-base)"
|
||||||
}
|
}
|
||||||
|
|
||||||
const kindDotColor = (kind: Kind) => {
|
const kindDotColor = (kind: Kind) => {
|
||||||
if (kind === "add") return "background-color: var(--icon-diff-add-base)"
|
if (kind === "add") return "background-color: var(--icon-diff-add-base)"
|
||||||
if (kind === "del") return "background-color: var(--icon-diff-delete-base)"
|
if (kind === "del") return "background-color: var(--icon-diff-delete-base)"
|
||||||
return "background-color: var(--icon-warning-active)"
|
return "background-color: var(--icon-diff-modified-base)"
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleKind = (node: FileNode, kinds?: ReadonlyMap<string, Kind>, marks?: Set<string>) => {
|
const visibleKind = (node: FileNode, kinds?: ReadonlyMap<string, Kind>, marks?: Set<string>) => {
|
||||||
@@ -447,12 +447,13 @@ export default function FileTree(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
<div data-component="filetree" class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
||||||
<For each={nodes()}>
|
<For each={nodes()}>
|
||||||
{(node) => {
|
{(node) => {
|
||||||
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
||||||
const deep = () => deeps().get(node.path) ?? -1
|
const deep = () => deeps().get(node.path) ?? -1
|
||||||
const kind = () => visibleKind(node, kinds(), marks())
|
const kind = () => visibleKind(node, kinds(), marks())
|
||||||
|
const active = () => !!kind() && !node.ignored
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -530,7 +531,37 @@ export default function FileTree(props: {
|
|||||||
onClick={() => props.onFileClick?.(node)}
|
onClick={() => props.onFileClick?.(node)}
|
||||||
>
|
>
|
||||||
<div class="w-4 shrink-0" />
|
<div class="w-4 shrink-0" />
|
||||||
<FileIcon node={node} class="text-icon-weak size-4" />
|
<Switch>
|
||||||
|
<Match when={node.ignored}>
|
||||||
|
<FileIcon
|
||||||
|
node={node}
|
||||||
|
class="size-4 filetree-icon filetree-icon--mono"
|
||||||
|
style="color: var(--icon-weak-base)"
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={active()}>
|
||||||
|
<FileIcon
|
||||||
|
node={node}
|
||||||
|
class="size-4 filetree-icon filetree-icon--mono"
|
||||||
|
style={kindTextColor(kind()!)}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={!node.ignored}>
|
||||||
|
<span class="filetree-iconpair size-4">
|
||||||
|
<FileIcon
|
||||||
|
node={node}
|
||||||
|
class="size-4 filetree-icon filetree-icon--color opacity-0 group-hover/filetree:opacity-100"
|
||||||
|
/>
|
||||||
|
<FileIcon
|
||||||
|
node={node}
|
||||||
|
class="size-4 filetree-icon filetree-icon--mono group-hover/filetree:opacity-0"
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</FileTreeNode>
|
</FileTreeNode>
|
||||||
</FileTreeNodeTooltip>
|
</FileTreeNodeTooltip>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { createEffect, on, Component, Show, For, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
|
import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
import { createFocusSignal } from "@solid-primitives/active-element"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
@@ -109,6 +109,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
let slashPopoverRef!: HTMLDivElement
|
let slashPopoverRef!: HTMLDivElement
|
||||||
|
|
||||||
const mirror = { input: false }
|
const mirror = { input: false }
|
||||||
|
const inset = 44
|
||||||
|
|
||||||
const scrollCursorIntoView = () => {
|
const scrollCursorIntoView = () => {
|
||||||
const container = scrollRef
|
const container = scrollRef
|
||||||
@@ -118,7 +119,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const range = selection.getRangeAt(0)
|
const range = selection.getRangeAt(0)
|
||||||
if (!editorRef.contains(range.startContainer)) return
|
if (!editorRef.contains(range.startContainer)) return
|
||||||
|
|
||||||
const rect = range.getBoundingClientRect()
|
const cursor = getCursorPosition(editorRef)
|
||||||
|
const length = promptLength(prompt.current().filter((part) => part.type !== "image"))
|
||||||
|
if (cursor >= length) {
|
||||||
|
container.scrollTop = container.scrollHeight
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = range.getClientRects().item(0) ?? range.getBoundingClientRect()
|
||||||
if (!rect.height) return
|
if (!rect.height) return
|
||||||
|
|
||||||
const containerRect = container.getBoundingClientRect()
|
const containerRect = container.getBoundingClientRect()
|
||||||
@@ -131,8 +139,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bottom > container.scrollTop + container.clientHeight - padding) {
|
if (bottom > container.scrollTop + container.clientHeight - inset) {
|
||||||
container.scrollTop = bottom - container.clientHeight + padding
|
container.scrollTop = bottom - container.clientHeight + inset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +250,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return messages.some((m) => m.role === "user")
|
return messages.some((m) => m.role === "user")
|
||||||
})
|
})
|
||||||
|
|
||||||
const MAX_HISTORY = 100
|
|
||||||
const [history, setHistory] = persisted(
|
const [history, setHistory] = persisted(
|
||||||
Persist.global("prompt-history", ["prompt-history.v1"]),
|
Persist.global("prompt-history", ["prompt-history.v1"]),
|
||||||
createStore<{
|
createStore<{
|
||||||
@@ -312,6 +319,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
requestAnimationFrame(() => editorRef?.focus())
|
requestAnimationFrame(() => editorRef?.focus())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shellModeKey = "mod+shift+x"
|
||||||
|
const normalModeKey = "mod+shift+e"
|
||||||
|
|
||||||
command.register("prompt-input", () => [
|
command.register("prompt-input", () => [
|
||||||
{
|
{
|
||||||
id: "file.attach",
|
id: "file.attach",
|
||||||
@@ -321,6 +331,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
disabled: store.mode !== "normal",
|
disabled: store.mode !== "normal",
|
||||||
onSelect: pick,
|
onSelect: pick,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "prompt.mode.shell",
|
||||||
|
title: language.t("command.prompt.mode.shell"),
|
||||||
|
category: language.t("command.category.session"),
|
||||||
|
keybind: shellModeKey,
|
||||||
|
disabled: store.mode === "shell",
|
||||||
|
onSelect: () => setMode("shell"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "prompt.mode.normal",
|
||||||
|
title: language.t("command.prompt.mode.normal"),
|
||||||
|
category: language.t("command.category.session"),
|
||||||
|
keybind: normalModeKey,
|
||||||
|
disabled: store.mode === "normal",
|
||||||
|
onSelect: () => setMode("normal"),
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const closePopover = () => setStore("popover", null)
|
const closePopover = () => setStore("popover", null)
|
||||||
@@ -377,15 +403,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const [composing, setComposing] = createSignal(false)
|
const [composing, setComposing] = createSignal(false)
|
||||||
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
|
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
|
||||||
|
|
||||||
createEffect(() => {
|
const handleBlur = () => {
|
||||||
if (!isFocused()) closePopover()
|
closePopover()
|
||||||
})
|
setComposing(false)
|
||||||
|
}
|
||||||
// Safety: reset composing state on focus change to prevent stuck state
|
|
||||||
// This handles edge cases where compositionend event may not fire
|
|
||||||
createEffect(() => {
|
|
||||||
if (!isFocused()) setComposing(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
const agentList = createMemo(() =>
|
const agentList = createMemo(() =>
|
||||||
sync.data.agent
|
sync.data.agent
|
||||||
@@ -1060,8 +1081,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
removeLabel={language.t("prompt.attachment.remove")}
|
removeLabel={language.t("prompt.attachment.remove")}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="relative max-h-[240px] overflow-y-auto no-scrollbar"
|
class="relative"
|
||||||
ref={(el) => (scrollRef = el)}
|
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
const target = e.target
|
const target = e.target
|
||||||
if (!(target instanceof HTMLElement)) return
|
if (!(target instanceof HTMLElement)) return
|
||||||
@@ -1075,151 +1095,154 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
editorRef?.focus()
|
editorRef?.focus()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div class="relative max-h-[240px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
|
||||||
data-component="prompt-input"
|
|
||||||
ref={(el) => {
|
|
||||||
editorRef = el
|
|
||||||
props.ref?.(el)
|
|
||||||
}}
|
|
||||||
role="textbox"
|
|
||||||
aria-multiline="true"
|
|
||||||
aria-label={placeholder()}
|
|
||||||
contenteditable="true"
|
|
||||||
autocapitalize="off"
|
|
||||||
autocorrect="off"
|
|
||||||
spellcheck={false}
|
|
||||||
onInput={handleInput}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
onCompositionStart={() => setComposing(true)}
|
|
||||||
onCompositionEnd={() => setComposing(false)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
classList={{
|
|
||||||
"select-text": true,
|
|
||||||
"w-full pl-3 pr-2 pt-2 pb-12 text-14-regular leading-[var(--line-height-large)] text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
|
||||||
"[&_[data-type=file]]:text-syntax-property": true,
|
|
||||||
"[&_[data-type=agent]]:text-syntax-type": true,
|
|
||||||
"font-mono!": store.mode === "shell",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Show when={!prompt.dirty()}>
|
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 pb-12 text-14-regular leading-[var(--line-height-large)] text-text-weak pointer-events-none whitespace-nowrap truncate"
|
data-component="prompt-input"
|
||||||
classList={{ "font-mono!": store.mode === "shell" }}
|
ref={(el) => {
|
||||||
|
editorRef = el
|
||||||
|
props.ref?.(el)
|
||||||
|
}}
|
||||||
|
role="textbox"
|
||||||
|
aria-multiline="true"
|
||||||
|
aria-label={placeholder()}
|
||||||
|
contenteditable="true"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck={false}
|
||||||
|
onInput={handleInput}
|
||||||
|
onPaste={handlePaste}
|
||||||
|
onCompositionStart={() => setComposing(true)}
|
||||||
|
onCompositionEnd={() => setComposing(false)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
classList={{
|
||||||
|
"select-text": true,
|
||||||
|
"w-full pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
||||||
|
"[&_[data-type=file]]:text-syntax-property": true,
|
||||||
|
"[&_[data-type=agent]]:text-syntax-type": true,
|
||||||
|
"font-mono!": store.mode === "shell",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Show when={!prompt.dirty()}>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||||
|
classList={{ "font-mono!": store.mode === "shell" }}
|
||||||
|
>
|
||||||
|
{placeholder()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept={ACCEPTED_FILE_TYPES.join(",")}
|
||||||
|
class="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.currentTarget.files?.[0]
|
||||||
|
if (file) addImageAttachment(file)
|
||||||
|
e.currentTarget.value = ""
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
aria-hidden={store.mode !== "normal"}
|
||||||
|
class="flex items-center gap-1 transition-all duration-200 ease-out"
|
||||||
|
classList={{
|
||||||
|
"opacity-100 translate-y-0 scale-100 pointer-events-auto": store.mode === "normal",
|
||||||
|
"opacity-0 translate-y-2 scale-95 pointer-events-none": store.mode !== "normal",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{placeholder()}
|
<TooltipKeybind
|
||||||
|
placement="top"
|
||||||
|
title={language.t("prompt.action.attachFile")}
|
||||||
|
keybind={command.keybind("file.attach")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
data-action="prompt-attach"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
class="size-8 p-0"
|
||||||
|
onClick={pick}
|
||||||
|
disabled={store.mode !== "normal"}
|
||||||
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
|
aria-label={language.t("prompt.action.attachFile")}
|
||||||
|
>
|
||||||
|
<Icon name="plus" class="size-4.5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
placement="top"
|
||||||
|
inactive={!prompt.dirty() && !working()}
|
||||||
|
value={
|
||||||
|
<Switch>
|
||||||
|
<Match when={working()}>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{language.t("prompt.action.stop")}</span>
|
||||||
|
<span class="text-icon-base text-12-medium text-[10px]!">{language.t("common.key.esc")}</span>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{language.t("prompt.action.send")}</span>
|
||||||
|
<Icon name="enter" size="small" class="text-icon-base" />
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
data-action="prompt-submit"
|
||||||
|
type="submit"
|
||||||
|
disabled={store.mode !== "normal" || (!prompt.dirty() && !working() && commentCount() === 0)}
|
||||||
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
|
icon={working() ? "stop" : "arrow-up"}
|
||||||
|
variant="primary"
|
||||||
|
class="size-8"
|
||||||
|
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={store.mode === "normal" && permission.permissionsEnabled() && params.id}>
|
||||||
|
<div class="pointer-events-none absolute bottom-2 left-2">
|
||||||
|
<div class="pointer-events-auto">
|
||||||
|
<TooltipKeybind
|
||||||
|
placement="top"
|
||||||
|
gutter={8}
|
||||||
|
title={language.t("command.permissions.autoaccept.enable")}
|
||||||
|
keybind={command.keybind("permissions.autoaccept")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
data-action="prompt-permissions"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
|
||||||
|
classList={{
|
||||||
|
"_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
|
||||||
|
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
|
||||||
|
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
||||||
|
}}
|
||||||
|
aria-label={
|
||||||
|
permission.isAutoAccepting(params.id!, sdk.directory)
|
||||||
|
? language.t("command.permissions.autoaccept.disable")
|
||||||
|
: language.t("command.permissions.autoaccept.enable")
|
||||||
|
}
|
||||||
|
aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="chevron-double-right"
|
||||||
|
size="small"
|
||||||
|
classList={{ "text-icon-success-base": permission.isAutoAccepting(params.id!, sdk.directory) }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept={ACCEPTED_FILE_TYPES.join(",")}
|
|
||||||
class="hidden"
|
|
||||||
onChange={(e) => {
|
|
||||||
const file = e.currentTarget.files?.[0]
|
|
||||||
if (file) addImageAttachment(file)
|
|
||||||
e.currentTarget.value = ""
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
aria-hidden={store.mode !== "normal"}
|
|
||||||
class="flex items-center gap-1 transition-all duration-200 ease-out"
|
|
||||||
classList={{
|
|
||||||
"opacity-100 translate-y-0 scale-100 pointer-events-auto": store.mode === "normal",
|
|
||||||
"opacity-0 translate-y-2 scale-95 pointer-events-none": store.mode !== "normal",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TooltipKeybind
|
|
||||||
placement="top"
|
|
||||||
title={language.t("prompt.action.attachFile")}
|
|
||||||
keybind={command.keybind("file.attach")}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
data-action="prompt-attach"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
class="size-8 p-0"
|
|
||||||
onClick={pick}
|
|
||||||
disabled={store.mode !== "normal"}
|
|
||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
|
||||||
aria-label={language.t("prompt.action.attachFile")}
|
|
||||||
>
|
|
||||||
<Icon name="plus" class="size-4.5" />
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
placement="top"
|
|
||||||
inactive={!prompt.dirty() && !working()}
|
|
||||||
value={
|
|
||||||
<Switch>
|
|
||||||
<Match when={working()}>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>{language.t("prompt.action.stop")}</span>
|
|
||||||
<span class="text-icon-base text-12-medium text-[10px]!">{language.t("common.key.esc")}</span>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>{language.t("prompt.action.send")}</span>
|
|
||||||
<Icon name="enter" size="small" class="text-icon-base" />
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
data-action="prompt-submit"
|
|
||||||
type="submit"
|
|
||||||
disabled={store.mode !== "normal" || (!prompt.dirty() && !working() && commentCount() === 0)}
|
|
||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
|
||||||
icon={working() ? "stop" : "arrow-up"}
|
|
||||||
variant="primary"
|
|
||||||
class="size-8"
|
|
||||||
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={store.mode === "normal" && permission.permissionsEnabled() && params.id}>
|
|
||||||
<div class="pointer-events-none absolute bottom-2 left-2">
|
|
||||||
<div class="pointer-events-auto">
|
|
||||||
<TooltipKeybind
|
|
||||||
placement="top"
|
|
||||||
gutter={8}
|
|
||||||
title={language.t("command.permissions.autoaccept.enable")}
|
|
||||||
keybind={command.keybind("permissions.autoaccept")}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
data-action="prompt-permissions"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
|
|
||||||
classList={{
|
|
||||||
"_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
|
|
||||||
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
|
|
||||||
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
|
||||||
}}
|
|
||||||
aria-label={
|
|
||||||
permission.isAutoAccepting(params.id!, sdk.directory)
|
|
||||||
? language.t("command.permissions.autoaccept.disable")
|
|
||||||
: language.t("command.permissions.autoaccept.enable")
|
|
||||||
}
|
|
||||||
aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="chevron-double-right"
|
|
||||||
size="small"
|
|
||||||
classList={{ "text-icon-success-base": permission.isAutoAccepting(params.id!, sdk.directory) }}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</form>
|
</form>
|
||||||
<Show when={store.mode === "normal" || store.mode === "shell"}>
|
<Show when={store.mode === "normal" || store.mode === "shell"}>
|
||||||
<div class="-mt-3.5 bg-background-base border border-border-weak-base relative z-0 rounded-[12px] rounded-tl-0 rounded-tr-0 overflow-clip">
|
<div class="-mt-3.5 bg-background-base border border-border-weak-base relative z-0 rounded-[12px] rounded-tl-0 rounded-tr-0 overflow-clip">
|
||||||
@@ -1331,29 +1354,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="shrink-0" data-component="prompt-mode-toggle">
|
||||||
<div class="shrink-0">
|
<RadioGroup
|
||||||
<div data-component="prompt-mode-toggle">
|
options={["shell", "normal"] as const}
|
||||||
<RadioGroup
|
current={store.mode}
|
||||||
options={["shell", "normal"] as const}
|
value={(mode) => mode}
|
||||||
current={store.mode}
|
label={(mode) => (
|
||||||
onSelect={(mode) => mode && setMode(mode)}
|
<TooltipKeybind
|
||||||
label={(mode) => (
|
placement="top"
|
||||||
<div class="flex size-full items-center justify-center">
|
gutter={4}
|
||||||
<Icon
|
openDelay={2000}
|
||||||
name={mode === "shell" ? "console" : "prompt"}
|
title={language.t(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
|
||||||
class="size-[18px]"
|
keybind={command.keybind(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
|
||||||
classList={{
|
class="size-full flex items-center justify-center"
|
||||||
"text-icon-strong-base": mode === "shell" && store.mode === "shell",
|
>
|
||||||
"text-icon-interactive-base": mode === "normal" && store.mode === "normal",
|
<Icon
|
||||||
"text-icon-weak": store.mode !== mode,
|
name={mode === "shell" ? "console" : "prompt"}
|
||||||
}}
|
class="size-[18px]"
|
||||||
/>
|
classList={{
|
||||||
<span class="sr-only">{mode === "shell" ? language.t("prompt.mode.shell") : mode}</span>
|
"text-icon-strong-base": store.mode === mode,
|
||||||
</div>
|
"text-icon-weak": store.mode !== mode,
|
||||||
)}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</TooltipKeybind>
|
||||||
|
)}
|
||||||
|
onSelect={(mode) => mode && setMode(mode)}
|
||||||
|
fill
|
||||||
|
pad="none"
|
||||||
|
class="w-[68px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,24 +12,27 @@ let selected = "/repo/worktree-a"
|
|||||||
|
|
||||||
const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
||||||
|
|
||||||
const clientFor = (directory: string) => ({
|
const clientFor = (directory: string) => {
|
||||||
session: {
|
createdClients.push(directory)
|
||||||
create: async () => {
|
return {
|
||||||
createdSessions.push(directory)
|
session: {
|
||||||
return { data: { id: `session-${createdSessions.length}` } }
|
create: async () => {
|
||||||
|
createdSessions.push(directory)
|
||||||
|
return { data: { id: `session-${createdSessions.length}` } }
|
||||||
|
},
|
||||||
|
shell: async () => {
|
||||||
|
sentShell.push(directory)
|
||||||
|
return { data: undefined }
|
||||||
|
},
|
||||||
|
prompt: async () => ({ data: undefined }),
|
||||||
|
command: async () => ({ data: undefined }),
|
||||||
|
abort: async () => ({ data: undefined }),
|
||||||
},
|
},
|
||||||
shell: async () => {
|
worktree: {
|
||||||
sentShell.push(directory)
|
create: async () => ({ data: { directory: `${directory}/new` } }),
|
||||||
return { data: undefined }
|
|
||||||
},
|
},
|
||||||
prompt: async () => ({ data: undefined }),
|
}
|
||||||
command: async () => ({ data: undefined }),
|
}
|
||||||
abort: async () => ({ data: undefined }),
|
|
||||||
},
|
|
||||||
worktree: {
|
|
||||||
create: async () => ({ data: { directory: `${directory}/new` } }),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const rootClient = clientFor("/repo/main")
|
const rootClient = clientFor("/repo/main")
|
||||||
@@ -88,11 +91,17 @@ beforeAll(async () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@/context/sdk", () => ({
|
mock.module("@/context/sdk", () => ({
|
||||||
useSDK: () => ({
|
useSDK: () => {
|
||||||
directory: "/repo/main",
|
const sdk = {
|
||||||
client: rootClient,
|
directory: "/repo/main",
|
||||||
url: "http://localhost:4096",
|
client: rootClient,
|
||||||
}),
|
url: "http://localhost:4096",
|
||||||
|
createClient(opts: any) {
|
||||||
|
return clientFor(opts.directory)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return sdk
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@/context/sync", () => ({
|
mock.module("@/context/sync", () => ({
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import { Accessor } from "solid-js"
|
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
|
||||||
import { createOpencodeClient, type Message } from "@opencode-ai/sdk/v2/client"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { useLocal } from "@/context/local"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { usePrompt, type ImageAttachmentPart, type Prompt } from "@/context/prompt"
|
import type { Accessor } from "solid-js"
|
||||||
|
import type { FileSelection } from "@/context/file"
|
||||||
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { useLocal } from "@/context/local"
|
||||||
|
import { type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||||
import type { FileSelection } from "@/context/file"
|
|
||||||
import { setCursorPosition } from "./editor-dom"
|
|
||||||
import { buildRequestParts } from "./build-request-parts"
|
import { buildRequestParts } from "./build-request-parts"
|
||||||
|
import { setCursorPosition } from "./editor-dom"
|
||||||
|
|
||||||
type PendingPrompt = {
|
type PendingPrompt = {
|
||||||
abort: AbortController
|
abort: AbortController
|
||||||
@@ -56,7 +55,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const platform = usePlatform()
|
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -175,9 +173,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sessionDirectory !== projectDirectory) {
|
if (sessionDirectory !== projectDirectory) {
|
||||||
client = createOpencodeClient({
|
client = sdk.createClient({
|
||||||
baseUrl: sdk.url,
|
|
||||||
fetch: platform.fetch,
|
|
||||||
directory: sessionDirectory,
|
directory: sessionDirectory,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
})
|
})
|
||||||
@@ -372,7 +368,10 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const timer = { id: undefined as number | undefined }
|
const timer = { id: undefined as number | undefined }
|
||||||
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||||
timer.id = window.setTimeout(() => {
|
timer.id = window.setTimeout(() => {
|
||||||
resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") })
|
resolve({
|
||||||
|
status: "failed",
|
||||||
|
message: language.t("workspace.error.stillPreparing"),
|
||||||
|
})
|
||||||
}, timeoutMs)
|
}, timeoutMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
|
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
@@ -232,9 +233,11 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="question-prompt" ref={(el) => (root = el)}>
|
<DockPrompt
|
||||||
<div data-slot="question-body">
|
kind="question"
|
||||||
<div data-slot="question-header">
|
ref={(el) => (root = el)}
|
||||||
|
header={
|
||||||
|
<>
|
||||||
<div data-slot="question-header-title">{summary()}</div>
|
<div data-slot="question-header-title">{summary()}</div>
|
||||||
<div data-slot="question-progress">
|
<div data-slot="question-progress">
|
||||||
<For each={questions()}>
|
<For each={questions()}>
|
||||||
@@ -254,172 +257,169 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
}
|
||||||
<div data-slot="question-content">
|
footer={
|
||||||
<div data-slot="question-text">{question()?.question}</div>
|
<>
|
||||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
<Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
|
||||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
{language.t("ui.common.dismiss")}
|
||||||
</Show>
|
</Button>
|
||||||
<div data-slot="question-options">
|
<div data-slot="question-footer-actions">
|
||||||
<For each={options()}>
|
<Show when={store.tab > 0}>
|
||||||
{(opt, i) => {
|
<Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
|
||||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
{language.t("ui.common.back")}
|
||||||
return (
|
</Button>
|
||||||
<button
|
</Show>
|
||||||
data-slot="question-option"
|
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
|
||||||
data-picked={picked()}
|
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
|
||||||
role={multi() ? "checkbox" : "radio"}
|
</Button>
|
||||||
aria-checked={picked()}
|
</div>
|
||||||
disabled={store.sending}
|
</>
|
||||||
onClick={() => selectOption(i())}
|
}
|
||||||
>
|
>
|
||||||
<span data-slot="question-option-check" aria-hidden="true">
|
<div data-slot="question-text">{question()?.question}</div>
|
||||||
<span
|
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||||
data-slot="question-option-box"
|
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||||
data-type={multi() ? "checkbox" : "radio"}
|
</Show>
|
||||||
data-picked={picked()}
|
<div data-slot="question-options">
|
||||||
>
|
<For each={options()}>
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
{(opt, i) => {
|
||||||
<Icon name="check-small" size="small" />
|
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||||
</Show>
|
return (
|
||||||
</span>
|
<button
|
||||||
</span>
|
|
||||||
<span data-slot="question-option-main">
|
|
||||||
<span data-slot="option-label">{opt.label}</span>
|
|
||||||
<Show when={opt.description}>
|
|
||||||
<span data-slot="option-description">{opt.description}</span>
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={store.editing}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
data-slot="question-option"
|
|
||||||
data-custom="true"
|
|
||||||
data-picked={on()}
|
|
||||||
role={multi() ? "checkbox" : "radio"}
|
|
||||||
aria-checked={on()}
|
|
||||||
disabled={store.sending}
|
|
||||||
onClick={customOpen}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-slot="question-option-check"
|
|
||||||
aria-hidden="true"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
customToggle()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
|
||||||
<Icon name="check-small" size="small" />
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span data-slot="question-option-main">
|
|
||||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
|
||||||
<span data-slot="option-description">
|
|
||||||
{input() || language.t("ui.question.custom.placeholder")}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
data-slot="question-option"
|
data-slot="question-option"
|
||||||
data-custom="true"
|
data-picked={picked()}
|
||||||
data-picked={on()}
|
|
||||||
role={multi() ? "checkbox" : "radio"}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
aria-checked={on()}
|
aria-checked={picked()}
|
||||||
onMouseDown={(e) => {
|
disabled={store.sending}
|
||||||
if (store.sending) {
|
onClick={() => selectOption(i())}
|
||||||
e.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.target instanceof HTMLTextAreaElement) return
|
|
||||||
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
|
||||||
if (input instanceof HTMLTextAreaElement) input.focus()
|
|
||||||
}}
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
commitCustom()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span
|
<span data-slot="question-option-check" aria-hidden="true">
|
||||||
data-slot="question-option-check"
|
<span
|
||||||
aria-hidden="true"
|
data-slot="question-option-box"
|
||||||
onClick={(e) => {
|
data-type={multi() ? "checkbox" : "radio"}
|
||||||
e.preventDefault()
|
data-picked={picked()}
|
||||||
e.stopPropagation()
|
>
|
||||||
customToggle()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||||
<Icon name="check-small" size="small" />
|
<Icon name="check-small" size="small" />
|
||||||
</Show>
|
</Show>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span data-slot="question-option-main">
|
<span data-slot="question-option-main">
|
||||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
<span data-slot="option-label">{opt.label}</span>
|
||||||
<textarea
|
<Show when={opt.description}>
|
||||||
ref={(el) =>
|
<span data-slot="option-description">{opt.description}</span>
|
||||||
setTimeout(() => {
|
</Show>
|
||||||
el.focus()
|
|
||||||
el.style.height = "0px"
|
|
||||||
el.style.height = `${el.scrollHeight}px`
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
data-slot="question-custom-input"
|
|
||||||
placeholder={language.t("ui.question.custom.placeholder")}
|
|
||||||
value={input()}
|
|
||||||
rows={1}
|
|
||||||
disabled={store.sending}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault()
|
|
||||||
setStore("editing", false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.key !== "Enter" || e.shiftKey) return
|
|
||||||
e.preventDefault()
|
|
||||||
commitCustom()
|
|
||||||
}}
|
|
||||||
onInput={(e) => {
|
|
||||||
customUpdate(e.currentTarget.value)
|
|
||||||
e.currentTarget.style.height = "0px"
|
|
||||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</button>
|
||||||
</Show>
|
)
|
||||||
</div>
|
}}
|
||||||
</div>
|
</For>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-slot="question-footer">
|
<Show
|
||||||
<Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
|
when={store.editing}
|
||||||
{language.t("ui.common.dismiss")}
|
fallback={
|
||||||
</Button>
|
<button
|
||||||
<div data-slot="question-footer-actions">
|
data-slot="question-option"
|
||||||
<Show when={store.tab > 0}>
|
data-custom="true"
|
||||||
<Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
|
data-picked={on()}
|
||||||
{language.t("ui.common.back")}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
</Button>
|
aria-checked={on()}
|
||||||
</Show>
|
disabled={store.sending}
|
||||||
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
|
onClick={customOpen}
|
||||||
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
|
>
|
||||||
</Button>
|
<span
|
||||||
</div>
|
data-slot="question-option-check"
|
||||||
|
aria-hidden="true"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
customToggle()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||||
|
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||||
|
<Icon name="check-small" size="small" />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span data-slot="question-option-main">
|
||||||
|
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||||
|
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
data-slot="question-option"
|
||||||
|
data-custom="true"
|
||||||
|
data-picked={on()}
|
||||||
|
role={multi() ? "checkbox" : "radio"}
|
||||||
|
aria-checked={on()}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (store.sending) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.target instanceof HTMLTextAreaElement) return
|
||||||
|
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
||||||
|
if (input instanceof HTMLTextAreaElement) input.focus()
|
||||||
|
}}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
commitCustom()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-slot="question-option-check"
|
||||||
|
aria-hidden="true"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
customToggle()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||||
|
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||||
|
<Icon name="check-small" size="small" />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span data-slot="question-option-main">
|
||||||
|
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||||
|
<textarea
|
||||||
|
ref={(el) =>
|
||||||
|
setTimeout(() => {
|
||||||
|
el.focus()
|
||||||
|
el.style.height = "0px"
|
||||||
|
el.style.height = `${el.scrollHeight}px`
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
data-slot="question-custom-input"
|
||||||
|
placeholder={language.t("ui.question.custom.placeholder")}
|
||||||
|
value={input()}
|
||||||
|
rows={1}
|
||||||
|
disabled={store.sending}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
setStore("editing", false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key !== "Enter" || e.shiftKey) return
|
||||||
|
e.preventDefault()
|
||||||
|
commitCustom()
|
||||||
|
}}
|
||||||
|
onInput={(e) => {
|
||||||
|
customUpdate(e.currentTarget.value)
|
||||||
|
e.currentTarget.style.height = "0px"
|
||||||
|
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DockPrompt>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { JSXElement, ParentProps, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
import {
|
||||||
import { serverDisplayName } from "@/context/server"
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
type JSXElement,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
type ParentProps,
|
||||||
|
Show,
|
||||||
|
} from "solid-js"
|
||||||
|
import { type ServerConnection, serverDisplayName } from "@/context/server"
|
||||||
import type { ServerHealth } from "@/utils/server-health"
|
import type { ServerHealth } from "@/utils/server-health"
|
||||||
|
|
||||||
interface ServerRowProps extends ParentProps {
|
interface ServerRowProps extends ParentProps {
|
||||||
url: string
|
conn: ServerConnection.Any
|
||||||
status?: ServerHealth
|
status?: ServerHealth
|
||||||
class?: string
|
class?: string
|
||||||
nameClass?: string
|
nameClass?: string
|
||||||
@@ -17,7 +26,7 @@ export function ServerRow(props: ServerRowProps) {
|
|||||||
const [truncated, setTruncated] = createSignal(false)
|
const [truncated, setTruncated] = createSignal(false)
|
||||||
let nameRef: HTMLSpanElement | undefined
|
let nameRef: HTMLSpanElement | undefined
|
||||||
let versionRef: HTMLSpanElement | undefined
|
let versionRef: HTMLSpanElement | undefined
|
||||||
const name = createMemo(() => serverDisplayName(props.url))
|
const name = createMemo(() => serverDisplayName(props.conn))
|
||||||
|
|
||||||
const check = () => {
|
const check = () => {
|
||||||
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
|
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
|
||||||
@@ -27,7 +36,7 @@ export function ServerRow(props: ServerRowProps) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
name()
|
name()
|
||||||
props.url
|
props.conn.http.url
|
||||||
props.status?.version
|
props.status?.version
|
||||||
queueMicrotask(check)
|
queueMicrotask(check)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useSync } from "@/context/sync"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { checksum } from "@opencode-ai/util/encode"
|
import { checksum } from "@opencode-ai/util/encode"
|
||||||
import { findLast } from "@opencode-ai/util/array"
|
import { findLast } from "@opencode-ai/util/array"
|
||||||
|
import { same } from "@/utils/same"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||||
@@ -16,13 +17,6 @@ import { getSessionContextMetrics } from "./session-context-metrics"
|
|||||||
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
|
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
|
||||||
import { createSessionContextFormatter } from "./session-context-format"
|
import { createSessionContextFormatter } from "./session-context-format"
|
||||||
|
|
||||||
interface SessionContextTabProps {
|
|
||||||
messages: () => Message[]
|
|
||||||
visibleUserMessages: () => UserMessage[]
|
|
||||||
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
|
||||||
info: () => ReturnType<ReturnType<typeof useSync>["session"]["get"]>
|
|
||||||
}
|
|
||||||
|
|
||||||
const BREAKDOWN_COLOR: Record<SessionContextBreakdownKey, string> = {
|
const BREAKDOWN_COLOR: Record<SessionContextBreakdownKey, string> = {
|
||||||
system: "var(--syntax-info)",
|
system: "var(--syntax-info)",
|
||||||
user: "var(--syntax-success)",
|
user: "var(--syntax-success)",
|
||||||
@@ -91,11 +85,45 @@ function RawMessage(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SessionContextTab(props: SessionContextTabProps) {
|
const emptyMessages: Message[] = []
|
||||||
|
const emptyUserMessages: UserMessage[] = []
|
||||||
|
|
||||||
|
export function SessionContextTab() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const layout = useLayout()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
|
|
||||||
|
const messages = createMemo(
|
||||||
|
() => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return emptyMessages
|
||||||
|
return (sync.data.message[id] ?? []) as Message[]
|
||||||
|
},
|
||||||
|
emptyMessages,
|
||||||
|
{ equals: same },
|
||||||
|
)
|
||||||
|
|
||||||
|
const userMessages = createMemo(
|
||||||
|
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||||
|
emptyUserMessages,
|
||||||
|
{ equals: same },
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibleUserMessages = createMemo(
|
||||||
|
() => {
|
||||||
|
const revert = info()?.revert?.messageID
|
||||||
|
if (!revert) return userMessages()
|
||||||
|
return userMessages().filter((m) => m.id < revert)
|
||||||
|
},
|
||||||
|
emptyUserMessages,
|
||||||
|
{ equals: same },
|
||||||
|
)
|
||||||
|
|
||||||
const usd = createMemo(
|
const usd = createMemo(
|
||||||
() =>
|
() =>
|
||||||
new Intl.NumberFormat(language.locale(), {
|
new Intl.NumberFormat(language.locale(), {
|
||||||
@@ -104,7 +132,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const metrics = createMemo(() => getSessionContextMetrics(props.messages(), sync.data.provider.all))
|
const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all))
|
||||||
const ctx = createMemo(() => metrics().context)
|
const ctx = createMemo(() => metrics().context)
|
||||||
const formatter = createMemo(() => createSessionContextFormatter(language.locale()))
|
const formatter = createMemo(() => createSessionContextFormatter(language.locale()))
|
||||||
|
|
||||||
@@ -113,7 +141,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const counts = createMemo(() => {
|
const counts = createMemo(() => {
|
||||||
const all = props.messages()
|
const all = messages()
|
||||||
const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0)
|
const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0)
|
||||||
const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0)
|
const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0)
|
||||||
return {
|
return {
|
||||||
@@ -124,7 +152,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const systemPrompt = createMemo(() => {
|
const systemPrompt = createMemo(() => {
|
||||||
const msg = findLast(props.visibleUserMessages(), (m) => !!m.system)
|
const msg = findLast(visibleUserMessages(), (m) => !!m.system)
|
||||||
const system = msg?.system
|
const system = msg?.system
|
||||||
if (!system) return
|
if (!system) return
|
||||||
const trimmed = system.trim()
|
const trimmed = system.trim()
|
||||||
@@ -146,12 +174,12 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
|
|
||||||
const breakdown = createMemo(
|
const breakdown = createMemo(
|
||||||
on(
|
on(
|
||||||
() => [ctx()?.message.id, ctx()?.input, props.messages().length, systemPrompt()],
|
() => [ctx()?.message.id, ctx()?.input, messages().length, systemPrompt()],
|
||||||
() => {
|
() => {
|
||||||
const c = ctx()
|
const c = ctx()
|
||||||
if (!c?.input) return []
|
if (!c?.input) return []
|
||||||
return estimateSessionContextBreakdown({
|
return estimateSessionContextBreakdown({
|
||||||
messages: props.messages(),
|
messages: messages(),
|
||||||
parts: sync.data.part as Record<string, Part[] | undefined>,
|
parts: sync.data.part as Record<string, Part[] | undefined>,
|
||||||
input: c.input,
|
input: c.input,
|
||||||
systemPrompt: systemPrompt(),
|
systemPrompt: systemPrompt(),
|
||||||
@@ -169,7 +197,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ label: "context.stats.session", value: () => props.info()?.title ?? params.id ?? "—" },
|
{ label: "context.stats.session", value: () => info()?.title ?? params.id ?? "—" },
|
||||||
{ label: "context.stats.messages", value: () => counts().all.toLocaleString(language.locale()) },
|
{ label: "context.stats.messages", value: () => counts().all.toLocaleString(language.locale()) },
|
||||||
{ label: "context.stats.provider", value: providerLabel },
|
{ label: "context.stats.provider", value: providerLabel },
|
||||||
{ label: "context.stats.model", value: modelLabel },
|
{ label: "context.stats.model", value: modelLabel },
|
||||||
@@ -186,7 +214,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) },
|
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) },
|
||||||
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) },
|
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) },
|
||||||
{ label: "context.stats.totalCost", value: cost },
|
{ label: "context.stats.totalCost", value: cost },
|
||||||
{ label: "context.stats.sessionCreated", value: () => formatter().time(props.info()?.time.created) },
|
{ label: "context.stats.sessionCreated", value: () => formatter().time(info()?.time.created) },
|
||||||
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
|
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
|
||||||
] satisfies { label: string; value: () => JSX.Element }[]
|
] satisfies { label: string; value: () => JSX.Element }[]
|
||||||
|
|
||||||
@@ -199,7 +227,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
const el = scroll
|
const el = scroll
|
||||||
if (!el) return
|
if (!el) return
|
||||||
|
|
||||||
const s = props.view()?.scroll("context")
|
const s = view().scroll("context")
|
||||||
if (!s) return
|
if (!s) return
|
||||||
|
|
||||||
if (el.scrollTop !== s.y) el.scrollTop = s.y
|
if (el.scrollTop !== s.y) el.scrollTop = s.y
|
||||||
@@ -220,13 +248,13 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
pending = undefined
|
pending = undefined
|
||||||
if (!next) return
|
if (!next) return
|
||||||
|
|
||||||
props.view().setScroll("context", next)
|
view().setScroll("context", next)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.messages().length,
|
() => messages().length,
|
||||||
() => {
|
() => {
|
||||||
requestAnimationFrame(restoreScroll)
|
requestAnimationFrame(restoreScroll)
|
||||||
},
|
},
|
||||||
@@ -300,7 +328,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||||
<Accordion multiple>
|
<Accordion multiple>
|
||||||
<For each={props.messages()}>
|
<For each={messages()}>
|
||||||
{(message) => (
|
{(message) => (
|
||||||
<RawMessage message={message} getParts={getParts} onRendered={restoreScroll} time={formatter().time} />
|
<RawMessage message={message} getParts={getParts} onRendered={restoreScroll} time={formatter().time} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -257,27 +257,12 @@ export function SessionHeader() {
|
|||||||
] as const
|
] as const
|
||||||
})
|
})
|
||||||
|
|
||||||
const checksReady = createMemo(() => {
|
|
||||||
if (platform.platform !== "desktop") return true
|
|
||||||
if (!platform.checkAppExists) return true
|
|
||||||
const list = apps()
|
|
||||||
return list.every((app) => exists[app.id] !== undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
||||||
const [menu, setMenu] = createStore({ open: false })
|
const [menu, setMenu] = createStore({ open: false })
|
||||||
|
|
||||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||||
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (platform.platform !== "desktop") return
|
|
||||||
if (!checksReady()) return
|
|
||||||
const value = prefs.app
|
|
||||||
if (options().some((o) => o.id === value)) return
|
|
||||||
setPrefs("app", options()[0]?.id ?? "finder")
|
|
||||||
})
|
|
||||||
|
|
||||||
const openDir = (app: OpenApp) => {
|
const openDir = (app: OpenApp) => {
|
||||||
const directory = projectDirectory()
|
const directory = projectDirectory()
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
@@ -319,9 +304,11 @@ export function SessionHeader() {
|
|||||||
<Show when={centerMount()}>
|
<Show when={centerMount()}>
|
||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount()}>
|
<Portal mount={mount()}>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
class="hidden md:flex w-[240px] max-w-full min-w-0 h-[24px] pl-0.5 pr-2 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
class="hidden md:flex w-[240px] max-w-full min-w-0 pl-0.5 pr-2 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel shadow-none cursor-default"
|
||||||
onClick={() => command.trigger("file.open")}
|
onClick={() => command.trigger("file.open")}
|
||||||
aria-label={language.t("session.header.searchFiles")}
|
aria-label={language.t("session.header.searchFiles")}
|
||||||
>
|
>
|
||||||
@@ -337,7 +324,7 @@ export function SessionHeader() {
|
|||||||
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind()}</Keybind>
|
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind()}</Keybind>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</Button>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -354,7 +341,7 @@ export function SessionHeader() {
|
|||||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="rounded-none h-full py-0 pr-2 pl-0.5 gap-1.5 border-none shadow-none"
|
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
|
||||||
onClick={copyPath}
|
onClick={copyPath}
|
||||||
aria-label={language.t("session.header.open.copyPath")}
|
aria-label={language.t("session.header.open.copyPath")}
|
||||||
>
|
>
|
||||||
@@ -398,7 +385,7 @@ export function SessionHeader() {
|
|||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
||||||
<DropdownMenu.RadioGroup
|
<DropdownMenu.RadioGroup
|
||||||
value={prefs.app}
|
value={current().id}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (!OPEN_APPS.includes(value as OpenApp)) return
|
if (!OPEN_APPS.includes(value as OpenApp)) return
|
||||||
setPrefs("app", value as OpenApp)
|
setPrefs("app", value as OpenApp)
|
||||||
@@ -464,11 +451,11 @@ export function SessionHeader() {
|
|||||||
triggerProps={{
|
triggerProps={{
|
||||||
variant: "ghost",
|
variant: "ghost",
|
||||||
class:
|
class:
|
||||||
"rounded-md h-[24px] px-3 border border-border-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-active",
|
"rounded-md h-[24px] px-3 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active",
|
||||||
classList: { "rounded-r-none": share.shareUrl() !== undefined },
|
classList: { "rounded-r-none": share.shareUrl() !== undefined },
|
||||||
style: { scale: 1 },
|
style: { scale: 1 },
|
||||||
}}
|
}}
|
||||||
trigger={language.t("session.share.action.share")}
|
trigger={<span class="text-12-regular">{language.t("session.share.action.share")}</span>}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Show
|
<Show
|
||||||
@@ -537,7 +524,7 @@ export function SessionHeader() {
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={share.state.copied ? "check" : "link"}
|
icon={share.state.copied ? "check" : "link"}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="rounded-l-none h-[24px] border border-border-base bg-surface-panel shadow-none"
|
class="rounded-l-none h-[24px] border border-border-weak-base bg-surface-panel shadow-none"
|
||||||
onClick={() => share.copyLink((error) => showRequestError(language, error))}
|
onClick={() => share.copyLink((error) => showRequestError(language, error))}
|
||||||
disabled={share.state.unshare}
|
disabled={share.state.unshare}
|
||||||
aria-label={
|
aria-label={
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
<SoundsSection />
|
<SoundsSection />
|
||||||
|
|
||||||
<Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled}>
|
{/*<Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled}>
|
||||||
{(_) => {
|
{(_) => {
|
||||||
const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
|
const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
|
||||||
const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest)
|
const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest)
|
||||||
@@ -457,7 +457,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Show>
|
</Show>*/}
|
||||||
|
|
||||||
<UpdatesSection />
|
<UpdatesSection />
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { createEffect, createMemo, createSignal, For, onCleanup, Show, type Accessor, type JSXElement } from "solid-js"
|
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
|
||||||
import { useNavigate } from "@solidjs/router"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { Popover } from "@opencode-ai/ui/popover"
|
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useSync } from "@/context/sync"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { type Accessor, createEffect, createMemo, createSignal, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||||
import { normalizeServerUrl, useServer } from "@/context/server"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { DialogSelectServer } from "./dialog-select-server"
|
|
||||||
import { ServerRow } from "@/components/server/server-row"
|
import { ServerRow } from "@/components/server/server-row"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
|
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||||
|
import { DialogSelectServer } from "./dialog-select-server"
|
||||||
|
|
||||||
const pollMs = 10_000
|
const pollMs = 10_000
|
||||||
|
|
||||||
@@ -32,9 +32,9 @@ const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listServersByHealth = (
|
const listServersByHealth = (
|
||||||
list: string[],
|
list: ServerConnection.Any[],
|
||||||
active: string | undefined,
|
active: ServerConnection.Key | undefined,
|
||||||
status: Record<string, ServerHealth | undefined>,
|
status: Record<ServerConnection.Key, ServerHealth | undefined>,
|
||||||
) => {
|
) => {
|
||||||
if (!list.length) return list
|
if (!list.length) return list
|
||||||
const order = new Map(list.map((url, index) => [url, index] as const))
|
const order = new Map(list.map((url, index) => [url, index] as const))
|
||||||
@@ -45,16 +45,16 @@ const listServersByHealth = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return list.slice().sort((a, b) => {
|
return list.slice().sort((a, b) => {
|
||||||
if (a === active) return -1
|
if (ServerConnection.key(a) === active) return -1
|
||||||
if (b === active) return 1
|
if (ServerConnection.key(b) === active) return 1
|
||||||
const diff = rank(status[a]) - rank(status[b])
|
const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)])
|
||||||
if (diff !== 0) return diff
|
if (diff !== 0) return diff
|
||||||
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const useServerHealth = (servers: Accessor<string[]>, fetcher: typeof fetch) => {
|
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, fetcher: typeof fetch) => {
|
||||||
const [status, setStatus] = createStore({} as Record<string, ServerHealth | undefined>)
|
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const list = servers()
|
const list = servers()
|
||||||
@@ -63,8 +63,8 @@ const useServerHealth = (servers: Accessor<string[]>, fetcher: typeof fetch) =>
|
|||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
const results: Record<string, ServerHealth> = {}
|
const results: Record<string, ServerHealth> = {}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
list.map(async (url) => {
|
list.map(async (conn) => {
|
||||||
results[url] = await checkServerHealth(url, fetcher)
|
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (dead) return
|
if (dead) return
|
||||||
@@ -82,7 +82,7 @@ const useServerHealth = (servers: Accessor<string[]>, fetcher: typeof fetch) =>
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
const useDefaultServerUrl = (
|
const useDefaultServerKey = (
|
||||||
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
|
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
|
||||||
) => {
|
) => {
|
||||||
const [url, setUrl] = createSignal<string | undefined>()
|
const [url, setUrl] = createSignal<string | undefined>()
|
||||||
@@ -117,7 +117,14 @@ const useDefaultServerUrl = (
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return { url, refresh: () => setTick((value) => value + 1) }
|
return {
|
||||||
|
key: () => {
|
||||||
|
const u = url()
|
||||||
|
if (!u) return
|
||||||
|
return ServerConnection.key({ type: "http", http: { url: u } })
|
||||||
|
},
|
||||||
|
refresh: () => setTick((value) => value + 1),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const useMcpToggle = (input: {
|
const useMcpToggle = (input: {
|
||||||
@@ -163,16 +170,16 @@ export function StatusPopover() {
|
|||||||
|
|
||||||
const fetcher = platform.fetch ?? globalThis.fetch
|
const fetcher = platform.fetch ?? globalThis.fetch
|
||||||
const servers = createMemo(() => {
|
const servers = createMemo(() => {
|
||||||
const current = server.url
|
const current = server.current
|
||||||
const list = server.list
|
const list = server.list
|
||||||
if (!current) return list
|
if (!current) return list
|
||||||
if (!list.includes(current)) return [current, ...list]
|
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
|
||||||
return [current, ...list.filter((item) => item !== current)]
|
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
|
||||||
})
|
})
|
||||||
const health = useServerHealth(servers, fetcher)
|
const health = useServerHealth(servers, fetcher)
|
||||||
const sortedServers = createMemo(() => listServersByHealth(servers(), server.url, health))
|
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
|
||||||
const mcp = useMcpToggle({ sync, sdk, language })
|
const mcp = useMcpToggle({ sync, sdk, language })
|
||||||
const defaultServer = useDefaultServerUrl(platform.getDefaultServerUrl)
|
const defaultServer = useDefaultServerKey(platform.getDefaultServerUrl)
|
||||||
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||||
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
||||||
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
|
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
|
||||||
@@ -196,7 +203,7 @@ export function StatusPopover() {
|
|||||||
triggerProps={{
|
triggerProps={{
|
||||||
variant: "ghost",
|
variant: "ghost",
|
||||||
class:
|
class:
|
||||||
"rounded-md h-[24px] pr-2 pl-0.5 gap-2 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-hover",
|
"rounded-md h-[24px] pr-3 pl-0.5 gap-2 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active",
|
||||||
style: { scale: 1 },
|
style: { scale: 1 },
|
||||||
}}
|
}}
|
||||||
trigger={
|
trigger={
|
||||||
@@ -251,8 +258,9 @@ export function StatusPopover() {
|
|||||||
<div class="flex flex-col px-2 pb-2">
|
<div class="flex flex-col px-2 pb-2">
|
||||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
||||||
<For each={sortedServers()}>
|
<For each={sortedServers()}>
|
||||||
{(url) => {
|
{(s) => {
|
||||||
const isBlocked = () => health[url]?.healthy === false
|
const key = ServerConnection.key(s)
|
||||||
|
const isBlocked = () => health[key]?.healthy === false
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -264,19 +272,19 @@ export function StatusPopover() {
|
|||||||
aria-disabled={isBlocked()}
|
aria-disabled={isBlocked()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isBlocked()) return
|
if (isBlocked()) return
|
||||||
server.setActive(url)
|
server.setActive(key)
|
||||||
navigate("/")
|
navigate("/")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ServerRow
|
<ServerRow
|
||||||
url={url}
|
conn={s}
|
||||||
status={health[url]}
|
status={health[key]}
|
||||||
dimmed={isBlocked()}
|
dimmed={isBlocked()}
|
||||||
class="flex items-center gap-2 w-full min-w-0"
|
class="flex items-center gap-2 w-full min-w-0"
|
||||||
nameClass="text-14-regular text-text-base truncate"
|
nameClass="text-14-regular text-text-base truncate"
|
||||||
versionClass="text-12-regular text-text-weak truncate"
|
versionClass="text-12-regular text-text-weak truncate"
|
||||||
badge={
|
badge={
|
||||||
<Show when={url === defaultServer.url()}>
|
<Show when={key === defaultServer.key()}>
|
||||||
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
||||||
{language.t("common.default")}
|
{language.t("common.default")}
|
||||||
</span>
|
</span>
|
||||||
@@ -284,7 +292,7 @@ export function StatusPopover() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<Show when={url === server.url}>
|
<Show when={server.current && key === ServerConnection.key(server.current)}>
|
||||||
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
|
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
|
||||||
</Show>
|
</Show>
|
||||||
</ServerRow>
|
</ServerRow>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
import { type HexColor, resolveThemeVariant, useTheme, withAlpha } from "@opencode-ai/ui/theme"
|
||||||
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
|
||||||
|
import { type ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
|
||||||
|
import { SerializeAddon } from "@/addons/serialize"
|
||||||
|
import { matchKeybind, parseKeybind } from "@/context/command"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useServer } from "@/context/server"
|
||||||
import { monoFontFamily, useSettings } from "@/context/settings"
|
import { monoFontFamily, useSettings } from "@/context/settings"
|
||||||
import { parseKeybind, matchKeybind } from "@/context/command"
|
import type { LocalPTY } from "@/context/terminal"
|
||||||
import { SerializeAddon } from "@/addons/serialize"
|
|
||||||
import { LocalPTY } from "@/context/terminal"
|
|
||||||
import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
||||||
import { terminalWriter } from "@/utils/terminal-writer"
|
import { terminalWriter } from "@/utils/terminal-writer"
|
||||||
|
|
||||||
@@ -106,8 +107,14 @@ const useTerminalUiBindings = (input: {
|
|||||||
input.container.addEventListener("pointerdown", input.handlePointerDown)
|
input.container.addEventListener("pointerdown", input.handlePointerDown)
|
||||||
input.cleanups.push(() => input.container.removeEventListener("pointerdown", input.handlePointerDown))
|
input.cleanups.push(() => input.container.removeEventListener("pointerdown", input.handlePointerDown))
|
||||||
|
|
||||||
input.container.addEventListener("click", input.handleLinkClick, { capture: true })
|
input.container.addEventListener("click", input.handleLinkClick, {
|
||||||
input.cleanups.push(() => input.container.removeEventListener("click", input.handleLinkClick, { capture: true }))
|
capture: true,
|
||||||
|
})
|
||||||
|
input.cleanups.push(() =>
|
||||||
|
input.container.removeEventListener("click", input.handleLinkClick, {
|
||||||
|
capture: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
input.term.textarea?.addEventListener("focus", handleTextareaFocus)
|
input.term.textarea?.addEventListener("focus", handleTextareaFocus)
|
||||||
input.term.textarea?.addEventListener("blur", handleTextareaBlur)
|
input.term.textarea?.addEventListener("blur", handleTextareaBlur)
|
||||||
@@ -148,6 +155,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const server = useServer()
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
|
||||||
let ws: WebSocket | undefined
|
let ws: WebSocket | undefined
|
||||||
@@ -346,7 +354,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
ghostty = g
|
ghostty = g
|
||||||
term = t
|
term = t
|
||||||
output = terminalWriter((data) => t.write(data))
|
output = terminalWriter((data, done) => t.write(data, done))
|
||||||
|
|
||||||
t.attachCustomKeyEventHandler((event) => {
|
t.attachCustomKeyEventHandler((event) => {
|
||||||
const key = event.key.toLowerCase()
|
const key = event.key.toLowerCase()
|
||||||
@@ -372,7 +380,13 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
serializeAddon = serializer
|
serializeAddon = serializer
|
||||||
|
|
||||||
t.open(container)
|
t.open(container)
|
||||||
useTerminalUiBindings({ container, term: t, cleanups, handlePointerDown, handleLinkClick })
|
useTerminalUiBindings({
|
||||||
|
container,
|
||||||
|
term: t,
|
||||||
|
cleanups,
|
||||||
|
handlePointerDown,
|
||||||
|
handleLinkClick,
|
||||||
|
})
|
||||||
|
|
||||||
focusTerminal()
|
focusTerminal()
|
||||||
|
|
||||||
@@ -428,10 +442,8 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
url.searchParams.set("directory", sdk.directory)
|
url.searchParams.set("directory", sdk.directory)
|
||||||
url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
|
url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
|
||||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
||||||
if (window.__OPENCODE__?.serverPassword) {
|
url.username = server.current?.http.username ?? ""
|
||||||
url.username = "opencode"
|
url.password = server.current?.http.password ?? ""
|
||||||
url.password = window.__OPENCODE__?.serverPassword
|
|
||||||
}
|
|
||||||
const socket = new WebSocket(url)
|
const socket = new WebSocket(url)
|
||||||
socket.binaryType = "arraybuffer"
|
socket.binaryType = "arraybuffer"
|
||||||
ws = socket
|
ws = socket
|
||||||
@@ -520,9 +532,19 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
disposed = true
|
disposed = true
|
||||||
if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
|
if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
|
||||||
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
|
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
|
||||||
output?.flush()
|
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close()
|
||||||
persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
|
|
||||||
cleanup()
|
const finalize = () => {
|
||||||
|
persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output) {
|
||||||
|
finalize()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output.flush(finalize)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
|
import type { Event } from "@opencode-ai/sdk/v2/client"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||||
import { batch, onCleanup } from "solid-js"
|
import { batch, onCleanup } from "solid-js"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import { createSdkForServer } from "@/utils/server"
|
||||||
import { usePlatform } from "./platform"
|
import { usePlatform } from "./platform"
|
||||||
import { useServer } from "./server"
|
import { useServer } from "./server"
|
||||||
|
|
||||||
@@ -17,20 +18,10 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
|
|
||||||
const password = typeof window === "undefined" ? undefined : window.__OPENCODE__?.serverPassword
|
|
||||||
|
|
||||||
const auth = (() => {
|
|
||||||
if (!password) return
|
|
||||||
if (!server.isLocal()) return
|
|
||||||
return {
|
|
||||||
Authorization: `Basic ${btoa(`opencode:${password}`)}`,
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const eventFetch = (() => {
|
const eventFetch = (() => {
|
||||||
if (!platform.fetch) return
|
if (!platform.fetch || !server.current) return
|
||||||
try {
|
try {
|
||||||
const url = new URL(server.url)
|
const url = new URL(server.current.http.url)
|
||||||
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
|
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
|
||||||
if (url.protocol === "http:" && !loopback) return platform.fetch
|
if (url.protocol === "http:" && !loopback) return platform.fetch
|
||||||
} catch {
|
} catch {
|
||||||
@@ -38,11 +29,13 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const eventSdk = createOpencodeClient({
|
const currentServer = server.current
|
||||||
baseUrl: server.url,
|
if (!currentServer) throw new Error("No server available")
|
||||||
|
|
||||||
|
const eventSdk = createSdkForServer({
|
||||||
signal: abort.signal,
|
signal: abort.signal,
|
||||||
fetch: eventFetch,
|
fetch: eventFetch,
|
||||||
headers: eventFetch ? undefined : auth,
|
server: currentServer.http,
|
||||||
})
|
})
|
||||||
const emitter = createGlobalEmitter<{
|
const emitter = createGlobalEmitter<{
|
||||||
[key: string]: Event
|
[key: string]: Event
|
||||||
@@ -133,7 +126,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
if (streamErrorLogged) return
|
if (streamErrorLogged) return
|
||||||
streamErrorLogged = true
|
streamErrorLogged = true
|
||||||
console.error("[global-sdk] event stream error", {
|
console.error("[global-sdk] event stream error", {
|
||||||
url: server.url,
|
url: currentServer.http.url,
|
||||||
fetch: eventFetch ? "platform" : "webview",
|
fetch: eventFetch ? "platform" : "webview",
|
||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
@@ -166,7 +159,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
if (!aborted(error) && !streamErrorLogged) {
|
if (!aborted(error) && !streamErrorLogged) {
|
||||||
streamErrorLogged = true
|
streamErrorLogged = true
|
||||||
console.error("[global-sdk] event stream failed", {
|
console.error("[global-sdk] event stream failed", {
|
||||||
url: server.url,
|
url: currentServer.http.url,
|
||||||
fetch: eventFetch ? "platform" : "webview",
|
fetch: eventFetch ? "platform" : "webview",
|
||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
@@ -200,12 +193,25 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
flush()
|
flush()
|
||||||
})
|
})
|
||||||
|
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createSdkForServer({
|
||||||
baseUrl: server.url,
|
server: server.current.http,
|
||||||
fetch: platform.fetch,
|
fetch: platform.fetch,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { url: server.url, client: sdk, event: emitter }
|
return {
|
||||||
|
url: currentServer.http.url,
|
||||||
|
client: sdk,
|
||||||
|
event: emitter,
|
||||||
|
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
||||||
|
const s = server.current
|
||||||
|
if (!s) throw new Error("Server not available")
|
||||||
|
return createSdkForServer({
|
||||||
|
server: s.http,
|
||||||
|
fetch: platform.fetch,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ describe("pickDirectoriesToEvict", () => {
|
|||||||
describe("loadRootSessionsWithFallback", () => {
|
describe("loadRootSessionsWithFallback", () => {
|
||||||
test("uses limited roots query when supported", async () => {
|
test("uses limited roots query when supported", async () => {
|
||||||
const calls: Array<{ directory: string; roots: true; limit?: number }> = []
|
const calls: Array<{ directory: string; roots: true; limit?: number }> = []
|
||||||
let fallback = 0
|
|
||||||
|
|
||||||
const result = await loadRootSessionsWithFallback({
|
const result = await loadRootSessionsWithFallback({
|
||||||
directory: "dir",
|
directory: "dir",
|
||||||
@@ -39,20 +38,15 @@ describe("loadRootSessionsWithFallback", () => {
|
|||||||
calls.push(query)
|
calls.push(query)
|
||||||
return { data: [] }
|
return { data: [] }
|
||||||
},
|
},
|
||||||
onFallback: () => {
|
|
||||||
fallback += 1
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.data).toEqual([])
|
expect(result.data).toEqual([])
|
||||||
expect(result.limited).toBe(true)
|
expect(result.limited).toBe(true)
|
||||||
expect(calls).toEqual([{ directory: "dir", roots: true, limit: 10 }])
|
expect(calls).toEqual([{ directory: "dir", roots: true, limit: 10 }])
|
||||||
expect(fallback).toBe(0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("falls back to full roots query on limited-query failure", async () => {
|
test("falls back to full roots query on limited-query failure", async () => {
|
||||||
const calls: Array<{ directory: string; roots: true; limit?: number }> = []
|
const calls: Array<{ directory: string; roots: true; limit?: number }> = []
|
||||||
let fallback = 0
|
|
||||||
|
|
||||||
const result = await loadRootSessionsWithFallback({
|
const result = await loadRootSessionsWithFallback({
|
||||||
directory: "dir",
|
directory: "dir",
|
||||||
@@ -62,9 +56,6 @@ describe("loadRootSessionsWithFallback", () => {
|
|||||||
if (query.limit) throw new Error("unsupported")
|
if (query.limit) throw new Error("unsupported")
|
||||||
return { data: [] }
|
return { data: [] }
|
||||||
},
|
},
|
||||||
onFallback: () => {
|
|
||||||
fallback += 1
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.data).toEqual([])
|
expect(result.data).toEqual([])
|
||||||
@@ -73,7 +64,6 @@ describe("loadRootSessionsWithFallback", () => {
|
|||||||
{ directory: "dir", roots: true, limit: 25 },
|
{ directory: "dir", roots: true, limit: 25 },
|
||||||
{ directory: "dir", roots: true },
|
{ directory: "dir", roots: true },
|
||||||
])
|
])
|
||||||
expect(fallback).toBe(1)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import {
|
import type {
|
||||||
type Config,
|
Config,
|
||||||
type Path,
|
OpencodeClient,
|
||||||
type Project,
|
Path,
|
||||||
type ProviderAuthResponse,
|
Project,
|
||||||
type ProviderListResponse,
|
ProviderAuthResponse,
|
||||||
type Todo,
|
ProviderListResponse,
|
||||||
createOpencodeClient,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import type { InitError } from "../pages/error"
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
createEffect,
|
createEffect,
|
||||||
untrack,
|
|
||||||
getOwner,
|
getOwner,
|
||||||
useContext,
|
Match,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
onMount,
|
onMount,
|
||||||
type ParentProps,
|
type ParentProps,
|
||||||
Switch,
|
Switch,
|
||||||
Match,
|
untrack,
|
||||||
|
useContext,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
|
||||||
import { usePlatform } from "./platform"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { createRefreshQueue } from "./global-sync/queue"
|
import type { InitError } from "../pages/error"
|
||||||
import { createChildStoreManager } from "./global-sync/child-store"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { trimSessions } from "./global-sync/session-trim"
|
|
||||||
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
|
||||||
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
|
||||||
import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
|
import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
|
||||||
import { sanitizeProject } from "./global-sync/utils"
|
import { createChildStoreManager } from "./global-sync/child-store"
|
||||||
|
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
||||||
|
import { createRefreshQueue } from "./global-sync/queue"
|
||||||
|
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
||||||
|
import { trimSessions } from "./global-sync/session-trim"
|
||||||
import type { ProjectMeta } from "./global-sync/types"
|
import type { ProjectMeta } from "./global-sync/types"
|
||||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||||
|
import { sanitizeProject } from "./global-sync/utils"
|
||||||
|
import { usePlatform } from "./platform"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -57,14 +57,6 @@ function errorMessage(error: unknown) {
|
|||||||
return "Unknown error"
|
return "Unknown error"
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDevStats(value: {
|
|
||||||
activeDirectoryStores: number
|
|
||||||
evictions: number
|
|
||||||
loadSessionsFullFetchFallback: number
|
|
||||||
}) {
|
|
||||||
;(globalThis as { __OPENCODE_GLOBAL_SYNC_STATS?: typeof value }).__OPENCODE_GLOBAL_SYNC_STATS = value
|
|
||||||
}
|
|
||||||
|
|
||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
@@ -72,12 +64,7 @@ function createGlobalSync() {
|
|||||||
const owner = getOwner()
|
const owner = getOwner()
|
||||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||||
|
|
||||||
const stats = {
|
const sdkCache = new Map<string, OpencodeClient>()
|
||||||
evictions: 0,
|
|
||||||
loadSessionsFallback: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
const sdkCache = new Map<string, ReturnType<typeof createOpencodeClient>>()
|
|
||||||
const booting = new Map<string, Promise<void>>()
|
const booting = new Map<string, Promise<void>>()
|
||||||
const sessionLoads = new Map<string, Promise<void>>()
|
const sessionLoads = new Map<string, Promise<void>>()
|
||||||
const sessionMeta = new Map<string, { limit: number }>()
|
const sessionMeta = new Map<string, { limit: number }>()
|
||||||
@@ -112,15 +99,6 @@ function createGlobalSync() {
|
|||||||
setGlobalStore("session_todo", sessionID, reconcile(todos, { key: "id" }))
|
setGlobalStore("session_todo", sessionID, reconcile(todos, { key: "id" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateStats = (activeDirectoryStores: number) => {
|
|
||||||
if (!import.meta.env.DEV) return
|
|
||||||
setDevStats({
|
|
||||||
activeDirectoryStores,
|
|
||||||
evictions: stats.evictions,
|
|
||||||
loadSessionsFullFetchFallback: stats.loadSessionsFallback,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const paused = () => untrack(() => globalStore.reload) !== undefined
|
const paused = () => untrack(() => globalStore.reload) !== undefined
|
||||||
|
|
||||||
const queue = createRefreshQueue({
|
const queue = createRefreshQueue({
|
||||||
@@ -131,11 +109,6 @@ function createGlobalSync() {
|
|||||||
|
|
||||||
const children = createChildStoreManager({
|
const children = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
markStats: updateStats,
|
|
||||||
incrementEvictions: () => {
|
|
||||||
stats.evictions += 1
|
|
||||||
updateStats(Object.keys(children.children).length)
|
|
||||||
},
|
|
||||||
isBooting: (directory) => booting.has(directory),
|
isBooting: (directory) => booting.has(directory),
|
||||||
isLoadingSessions: (directory) => sessionLoads.has(directory),
|
isLoadingSessions: (directory) => sessionLoads.has(directory),
|
||||||
onBootstrap: (directory) => {
|
onBootstrap: (directory) => {
|
||||||
@@ -151,9 +124,7 @@ function createGlobalSync() {
|
|||||||
const sdkFor = (directory: string) => {
|
const sdkFor = (directory: string) => {
|
||||||
const cached = sdkCache.get(directory)
|
const cached = sdkCache.get(directory)
|
||||||
if (cached) return cached
|
if (cached) return cached
|
||||||
const sdk = createOpencodeClient({
|
const sdk = globalSDK.createClient({
|
||||||
baseUrl: globalSDK.url,
|
|
||||||
fetch: platform.fetch,
|
|
||||||
directory,
|
directory,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
})
|
})
|
||||||
@@ -193,7 +164,10 @@ function createGlobalSync() {
|
|||||||
const [store, setStore] = children.child(directory, { bootstrap: false })
|
const [store, setStore] = children.child(directory, { bootstrap: false })
|
||||||
const meta = sessionMeta.get(directory)
|
const meta = sessionMeta.get(directory)
|
||||||
if (meta && meta.limit >= store.limit) {
|
if (meta && meta.limit >= store.limit) {
|
||||||
const next = trimSessions(store.session, { limit: store.limit, permission: store.permission })
|
const next = trimSessions(store.session, {
|
||||||
|
limit: store.limit,
|
||||||
|
permission: store.permission,
|
||||||
|
})
|
||||||
if (next.length !== store.session.length) {
|
if (next.length !== store.session.length) {
|
||||||
setStore("session", reconcile(next, { key: "id" }))
|
setStore("session", reconcile(next, { key: "id" }))
|
||||||
}
|
}
|
||||||
@@ -206,10 +180,6 @@ function createGlobalSync() {
|
|||||||
directory,
|
directory,
|
||||||
limit,
|
limit,
|
||||||
list: (query) => globalSDK.client.session.list(query),
|
list: (query) => globalSDK.client.session.list(query),
|
||||||
onFallback: () => {
|
|
||||||
stats.loadSessionsFallback += 1
|
|
||||||
updateStats(Object.keys(children.children).length)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
const nonArchived = (x.data ?? [])
|
const nonArchived = (x.data ?? [])
|
||||||
@@ -218,10 +188,17 @@ function createGlobalSync() {
|
|||||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||||
const limit = store.limit
|
const limit = store.limit
|
||||||
const childSessions = store.session.filter((s) => !!s.parentID)
|
const childSessions = store.session.filter((s) => !!s.parentID)
|
||||||
const sessions = trimSessions([...nonArchived, ...childSessions], { limit, permission: store.permission })
|
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
||||||
|
limit,
|
||||||
|
permission: store.permission,
|
||||||
|
})
|
||||||
setStore(
|
setStore(
|
||||||
"sessionTotal",
|
"sessionTotal",
|
||||||
estimateRootSessionTotal({ count: nonArchived.length, limit: x.limit, limited: x.limited }),
|
estimateRootSessionTotal({
|
||||||
|
count: nonArchived.length,
|
||||||
|
limit: x.limit,
|
||||||
|
limited: x.limited,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
sessionMeta.set(directory, { limit })
|
sessionMeta.set(directory, { limit })
|
||||||
@@ -331,7 +308,9 @@ function createGlobalSync() {
|
|||||||
await bootstrapGlobal({
|
await bootstrapGlobal({
|
||||||
globalSDK: globalSDK.client,
|
globalSDK: globalSDK.client,
|
||||||
connectErrorTitle: language.t("dialog.server.add.error"),
|
connectErrorTitle: language.t("dialog.server.add.error"),
|
||||||
connectErrorDescription: language.t("error.globalSync.connectFailed", { url: globalSDK.url }),
|
connectErrorDescription: language.t("error.globalSync.connectFailed", {
|
||||||
|
url: globalSDK.url,
|
||||||
|
}),
|
||||||
requestFailedTitle: language.t("common.requestFailed"),
|
requestFailedTitle: language.t("common.requestFailed"),
|
||||||
setGlobalStore,
|
setGlobalStore,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import {
|
import type {
|
||||||
type Config,
|
Config,
|
||||||
type Path,
|
OpencodeClient,
|
||||||
type PermissionRequest,
|
Path,
|
||||||
type Project,
|
PermissionRequest,
|
||||||
type ProviderAuthResponse,
|
Project,
|
||||||
type ProviderListResponse,
|
ProviderAuthResponse,
|
||||||
type QuestionRequest,
|
ProviderListResponse,
|
||||||
type Todo,
|
QuestionRequest,
|
||||||
createOpencodeClient,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { batch } from "solid-js"
|
import { batch } from "solid-js"
|
||||||
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import { retry } from "@opencode-ai/util/retry"
|
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { cmp, normalizeProviderList } from "./utils"
|
|
||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
|
import { cmp, normalizeProviderList } from "./utils"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -31,7 +31,7 @@ type GlobalStore = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function bootstrapGlobal(input: {
|
export async function bootstrapGlobal(input: {
|
||||||
globalSDK: ReturnType<typeof createOpencodeClient>
|
globalSDK: OpencodeClient
|
||||||
connectErrorTitle: string
|
connectErrorTitle: string
|
||||||
connectErrorDescription: string
|
connectErrorDescription: string
|
||||||
requestFailedTitle: string
|
requestFailedTitle: string
|
||||||
@@ -110,7 +110,7 @@ function groupBySession<T extends { id: string; sessionID: string }>(input: T[])
|
|||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
sdk: ReturnType<typeof createOpencodeClient>
|
sdk: OpencodeClient
|
||||||
store: Store<State>
|
store: Store<State>
|
||||||
setStore: SetStoreFunction<State>
|
setStore: SetStoreFunction<State>
|
||||||
vcsCache: VcsCache
|
vcsCache: VcsCache
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ describe("createChildStoreManager", () => {
|
|||||||
|
|
||||||
const manager = createChildStoreManager({
|
const manager = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
markStats() {},
|
|
||||||
incrementEvictions() {},
|
|
||||||
isBooting: () => false,
|
isBooting: () => false,
|
||||||
isLoadingSessions: () => false,
|
isLoadingSessions: () => false,
|
||||||
onBootstrap() {},
|
onBootstrap() {},
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
|||||||
|
|
||||||
export function createChildStoreManager(input: {
|
export function createChildStoreManager(input: {
|
||||||
owner: Owner
|
owner: Owner
|
||||||
markStats: (activeDirectoryStores: number) => void
|
|
||||||
incrementEvictions: () => void
|
|
||||||
isBooting: (directory: string) => boolean
|
isBooting: (directory: string) => boolean
|
||||||
isLoadingSessions: (directory: string) => boolean
|
isLoadingSessions: (directory: string) => boolean
|
||||||
onBootstrap: (directory: string) => void
|
onBootstrap: (directory: string) => void
|
||||||
@@ -102,7 +100,6 @@ export function createChildStoreManager(input: {
|
|||||||
}
|
}
|
||||||
delete children[directory]
|
delete children[directory]
|
||||||
input.onDispose(directory)
|
input.onDispose(directory)
|
||||||
input.markStats(Object.keys(children).length)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +117,6 @@ export function createChildStoreManager(input: {
|
|||||||
if (list.length === 0) return
|
if (list.length === 0) return
|
||||||
for (const directory of list) {
|
for (const directory of list) {
|
||||||
if (!disposeDirectory(directory)) continue
|
if (!disposeDirectory(directory)) continue
|
||||||
input.incrementEvictions()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +196,6 @@ export function createChildStoreManager(input: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
runWithOwner(input.owner, init)
|
runWithOwner(input.owner, init)
|
||||||
input.markStats(Object.keys(children).length)
|
|
||||||
}
|
}
|
||||||
mark(directory)
|
mark(directory)
|
||||||
const childStore = children[directory]
|
const childStore = children[directory]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export async function loadRootSessionsWithFallback(input: RootLoadArgs) {
|
|||||||
limited: true,
|
limited: true,
|
||||||
} as const
|
} as const
|
||||||
} catch {
|
} catch {
|
||||||
input.onFallback()
|
|
||||||
const result = await input.list({ directory: input.directory, roots: true })
|
const result = await input.list({ directory: input.directory, roots: true })
|
||||||
return {
|
return {
|
||||||
data: result.data,
|
data: result.data,
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ export type RootLoadArgs = {
|
|||||||
directory: string
|
directory: string
|
||||||
limit: number
|
limit: number
|
||||||
list: (query: { directory: string; roots: true; limit?: number }) => Promise<{ data?: Session[] }>
|
list: (query: { directory: string; roots: true; limit?: number }) => Promise<{ data?: Session[] }>
|
||||||
onFallback: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RootLoadResult = {
|
export type RootLoadResult = {
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ function detectLocale(): Locale {
|
|||||||
return "en"
|
return "en"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeLocale(value: string): Locale {
|
||||||
|
return LOCALES.includes(value as Locale) ? (value as Locale) : "en"
|
||||||
|
}
|
||||||
|
|
||||||
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
||||||
name: "Language",
|
name: "Language",
|
||||||
init: () => {
|
init: () => {
|
||||||
@@ -184,15 +188,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const locale = createMemo<Locale>(() =>
|
const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
|
||||||
LOCALES.includes(store.locale as Locale) ? (store.locale as Locale) : "en",
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const current = locale()
|
|
||||||
if (store.locale === current) return
|
|
||||||
setStore("locale", current)
|
|
||||||
})
|
|
||||||
|
|
||||||
const dict = createMemo<Dictionary>(() => DICT[locale()])
|
const dict = createMemo<Dictionary>(() => DICT[locale()])
|
||||||
|
|
||||||
@@ -213,7 +209,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
|||||||
label,
|
label,
|
||||||
t,
|
t,
|
||||||
setLocale(next: Locale) {
|
setLocale(next: Locale) {
|
||||||
setStore("locale", next)
|
setStore("locale", normalizeLocale(next))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
|
import type { Event } from "@opencode-ai/sdk/v2/client"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||||
import { createEffect, createMemo, onCleanup, type Accessor } from "solid-js"
|
import { type Accessor, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { usePlatform } from "./platform"
|
|
||||||
|
|
||||||
type SDKEventMap = {
|
type SDKEventMap = {
|
||||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||||
@@ -12,14 +11,11 @@ type SDKEventMap = {
|
|||||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
name: "SDK",
|
name: "SDK",
|
||||||
init: (props: { directory: Accessor<string> }) => {
|
init: (props: { directory: Accessor<string> }) => {
|
||||||
const platform = usePlatform()
|
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
|
|
||||||
const directory = createMemo(props.directory)
|
const directory = createMemo(props.directory)
|
||||||
const client = createMemo(() =>
|
const client = createMemo(() =>
|
||||||
createOpencodeClient({
|
globalSDK.createClient({
|
||||||
baseUrl: globalSDK.url,
|
|
||||||
fetch: platform.fetch,
|
|
||||||
directory: directory(),
|
directory: directory(),
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
}),
|
}),
|
||||||
@@ -45,6 +41,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||||||
get url() {
|
get url() {
|
||||||
return globalSDK.url
|
return globalSDK.url
|
||||||
},
|
},
|
||||||
|
createClient(opts: Parameters<typeof globalSDK.createClient>[0]) {
|
||||||
|
return globalSDK.createClient(opts)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -15,92 +15,122 @@ export function normalizeServerUrl(input: string) {
|
|||||||
return withProtocol.replace(/\/+$/, "")
|
return withProtocol.replace(/\/+$/, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serverDisplayName(url: string) {
|
export function serverDisplayName(conn?: ServerConnection.Any) {
|
||||||
if (!url) return ""
|
if (!conn) return ""
|
||||||
return url.replace(/^https?:\/\//, "").replace(/\/+$/, "")
|
if (conn.displayName) return conn.displayName
|
||||||
|
return conn.http.url.replace(/^https?:\/\//, "").replace(/\/+$/, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectsKey(url: string) {
|
function projectsKey(key: ServerConnection.Key) {
|
||||||
if (!url) return ""
|
if (!key) return ""
|
||||||
const host = url.replace(/^https?:\/\//, "").split(":")[0]
|
if (key === "sidecar") return "local"
|
||||||
|
const host = key.replace(/^https?:\/\//, "").split(":")[0]
|
||||||
if (host === "localhost" || host === "127.0.0.1") return "local"
|
if (host === "localhost" || host === "127.0.0.1") return "local"
|
||||||
return url
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace ServerConnection {
|
||||||
|
type Base = { displayName?: string }
|
||||||
|
|
||||||
|
export type HttpBase = {
|
||||||
|
url: string
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular web connections
|
||||||
|
export type Http = {
|
||||||
|
type: "http"
|
||||||
|
http: HttpBase
|
||||||
|
} & Base
|
||||||
|
|
||||||
|
export type Sidecar = {
|
||||||
|
type: "sidecar"
|
||||||
|
http: HttpBase
|
||||||
|
} & (
|
||||||
|
| // Regular desktop server
|
||||||
|
{ variant: "base" }
|
||||||
|
// WSL server (windows only)
|
||||||
|
| {
|
||||||
|
variant: "wsl"
|
||||||
|
distro: string
|
||||||
|
}
|
||||||
|
) &
|
||||||
|
Base
|
||||||
|
|
||||||
|
// Remote server desktop can SSH into
|
||||||
|
export type Ssh = {
|
||||||
|
type: "ssh"
|
||||||
|
host: string
|
||||||
|
// SSH client exposes an HTTP server for the app to use as a proxy
|
||||||
|
http: HttpBase
|
||||||
|
} & Base
|
||||||
|
|
||||||
|
export type Any =
|
||||||
|
| Http
|
||||||
|
// All these are desktop-only
|
||||||
|
| (Sidecar | Ssh)
|
||||||
|
|
||||||
|
export const key = (conn: Any): Key => {
|
||||||
|
switch (conn.type) {
|
||||||
|
case "http":
|
||||||
|
return Key.make(conn.http.url)
|
||||||
|
case "sidecar": {
|
||||||
|
if (conn.variant === "wsl") return Key.make(`wsl:${conn.distro}`)
|
||||||
|
return Key.make("sidecar")
|
||||||
|
}
|
||||||
|
case "ssh":
|
||||||
|
return Key.make(`ssh:${conn.host}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Key = string & { _brand: "Key" }
|
||||||
|
export const Key = { make: (v: string) => v as Key }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
|
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
|
||||||
name: "Server",
|
name: "Server",
|
||||||
init: (props: { defaultUrl: string; isSidecar?: boolean }) => {
|
init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("server", ["server.v3"]),
|
Persist.global("server", ["server.v3"]),
|
||||||
createStore({
|
createStore({
|
||||||
list: [] as string[],
|
list: [] as string[],
|
||||||
currentSidecarUrl: "",
|
|
||||||
projects: {} as Record<string, StoredProject[]>,
|
projects: {} as Record<string, StoredProject[]>,
|
||||||
lastProject: {} as Record<string, string>,
|
lastProject: {} as Record<string, string>,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
||||||
|
const servers = [
|
||||||
|
...(props.servers ?? []),
|
||||||
|
...store.list.map((value) => ({
|
||||||
|
type: "http" as const,
|
||||||
|
http: typeof value === "string" ? { url: value } : value,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
|
||||||
|
const deduped = new Map(servers.map((conn) => [ServerConnection.key(conn), conn]))
|
||||||
|
|
||||||
|
return [...deduped.values()]
|
||||||
|
})
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
active: "",
|
active: props.defaultServer,
|
||||||
healthy: undefined as boolean | undefined,
|
healthy: undefined as boolean | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const healthy = () => state.healthy
|
const healthy = () => state.healthy
|
||||||
|
|
||||||
const defaultUrl = () => normalizeServerUrl(props.defaultUrl)
|
function startHealthPolling(conn: ServerConnection.Any) {
|
||||||
|
|
||||||
function reconcileStartup() {
|
|
||||||
const fallback = defaultUrl()
|
|
||||||
if (!fallback) return
|
|
||||||
|
|
||||||
const previousSidecarUrl = normalizeServerUrl(store.currentSidecarUrl)
|
|
||||||
const list = previousSidecarUrl ? store.list.filter((url) => url !== previousSidecarUrl) : store.list
|
|
||||||
if (!props.isSidecar) {
|
|
||||||
batch(() => {
|
|
||||||
setStore("list", list)
|
|
||||||
if (store.currentSidecarUrl) setStore("currentSidecarUrl", "")
|
|
||||||
setState("active", fallback)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextList = list.includes(fallback) ? list : [...list, fallback]
|
|
||||||
batch(() => {
|
|
||||||
setStore("list", nextList)
|
|
||||||
setStore("currentSidecarUrl", fallback)
|
|
||||||
setState("active", fallback)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateServerList(url: string, remove = false) {
|
|
||||||
if (remove) {
|
|
||||||
const list = store.list.filter((x) => x !== url)
|
|
||||||
const next = state.active === url ? (list[0] ?? defaultUrl() ?? "") : state.active
|
|
||||||
batch(() => {
|
|
||||||
setStore("list", list)
|
|
||||||
setState("active", next)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
batch(() => {
|
|
||||||
if (!store.list.includes(url)) {
|
|
||||||
setStore("list", store.list.length, url)
|
|
||||||
}
|
|
||||||
setState("active", url)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function startHealthPolling(url: string) {
|
|
||||||
let alive = true
|
let alive = true
|
||||||
let busy = false
|
let busy = false
|
||||||
|
|
||||||
const run = () => {
|
const run = () => {
|
||||||
if (busy) return
|
if (busy) return
|
||||||
busy = true
|
busy = true
|
||||||
void check(url)
|
void check(conn)
|
||||||
.then((next) => {
|
.then((next) => {
|
||||||
if (!alive) return
|
if (!alive) return
|
||||||
setState("healthy", next)
|
setState("healthy", next)
|
||||||
@@ -118,59 +148,73 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActive(input: string) {
|
function setActive(input: ServerConnection.Key) {
|
||||||
const url = normalizeServerUrl(input)
|
if (state.active !== input) setState("active", input)
|
||||||
if (!url) return
|
|
||||||
setState("active", url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function add(input: string) {
|
function add(input: string) {
|
||||||
const url = normalizeServerUrl(input)
|
const url = normalizeServerUrl(input)
|
||||||
if (!url) return
|
if (!url) return
|
||||||
updateServerList(url)
|
return batch(() => {
|
||||||
|
const http: ServerConnection.HttpBase = { url }
|
||||||
|
if (!store.list.includes(url)) {
|
||||||
|
setStore("list", store.list.length, url)
|
||||||
|
}
|
||||||
|
const conn: ServerConnection.Http = { type: "http", http }
|
||||||
|
setState("active", ServerConnection.key(conn))
|
||||||
|
return conn
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(input: string) {
|
function remove(key: ServerConnection.Key) {
|
||||||
const url = normalizeServerUrl(input)
|
const list = store.list.filter((x) => x !== key)
|
||||||
if (!url) return
|
batch(() => {
|
||||||
updateServerList(url, true)
|
setStore("list", list)
|
||||||
|
if (state.active === key) {
|
||||||
|
const next = list[0]
|
||||||
|
setState("active", next ? ServerConnection.key({ type: "http", http: { url: next } }) : props.defaultServer)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!ready()) return
|
|
||||||
if (state.active) return
|
|
||||||
reconcileStartup()
|
|
||||||
})
|
|
||||||
|
|
||||||
const isReady = createMemo(() => ready() && !!state.active)
|
const isReady = createMemo(() => ready() && !!state.active)
|
||||||
|
|
||||||
const fetcher = platform.fetch ?? globalThis.fetch
|
const fetcher = platform.fetch ?? globalThis.fetch
|
||||||
const check = (url: string) => checkServerHealth(url, fetcher).then((x) => x.healthy)
|
const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http, fetcher).then((x) => x.healthy)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const url = state.active
|
const current_ = current()
|
||||||
if (!url) return
|
if (!current_) return
|
||||||
|
|
||||||
setState("healthy", undefined)
|
setState("healthy", undefined)
|
||||||
onCleanup(startHealthPolling(url))
|
onCleanup(startHealthPolling(current_))
|
||||||
})
|
})
|
||||||
|
|
||||||
const origin = createMemo(() => projectsKey(state.active))
|
const origin = createMemo(() => projectsKey(state.active))
|
||||||
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
||||||
const isLocal = createMemo(() => origin() === "local")
|
const current: Accessor<ServerConnection.Any | undefined> = createMemo(
|
||||||
|
() => allServers().find((s) => ServerConnection.key(s) === state.active) ?? allServers()[0],
|
||||||
|
)
|
||||||
|
const isLocal = createMemo(() => {
|
||||||
|
const c = current()
|
||||||
|
return c?.type === "sidecar" && c.variant === "base"
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: isReady,
|
ready: isReady,
|
||||||
healthy,
|
healthy,
|
||||||
isLocal,
|
isLocal,
|
||||||
get url() {
|
get key() {
|
||||||
return state.active
|
return state.active
|
||||||
},
|
},
|
||||||
get name() {
|
get name() {
|
||||||
return serverDisplayName(state.active)
|
return serverDisplayName(current())
|
||||||
},
|
},
|
||||||
get list() {
|
get list() {
|
||||||
return store.list
|
return allServers()
|
||||||
|
},
|
||||||
|
get current() {
|
||||||
|
return current()
|
||||||
},
|
},
|
||||||
setActive,
|
setActive,
|
||||||
add,
|
add,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
// @refresh reload
|
// @refresh reload
|
||||||
|
|
||||||
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import { AppBaseProviders, AppInterface } from "@/app"
|
import { AppBaseProviders, AppInterface } from "@/app"
|
||||||
import { Platform, PlatformProvider } from "@/context/platform"
|
import { type Platform, PlatformProvider } from "@/context/platform"
|
||||||
import { dict as en } from "@/i18n/en"
|
import { dict as en } from "@/i18n/en"
|
||||||
import { dict as zh } from "@/i18n/zh"
|
import { dict as zh } from "@/i18n/zh"
|
||||||
import { handleNotificationClick } from "@/utils/notification-click"
|
import { handleNotificationClick } from "@/utils/notification-click"
|
||||||
import pkg from "../package.json"
|
import pkg from "../package.json"
|
||||||
|
import { ServerConnection } from "./context/server"
|
||||||
|
|
||||||
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
|
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
|
||||||
|
|
||||||
@@ -107,12 +110,22 @@ const platform: Platform = {
|
|||||||
setDefaultServerUrl: writeDefaultServerUrl,
|
setDefaultServerUrl: writeDefaultServerUrl,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultUrl = iife(() => {
|
||||||
|
const lsDefault = readDefaultServerUrl()
|
||||||
|
if (lsDefault) return lsDefault
|
||||||
|
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||||
|
if (import.meta.env.DEV)
|
||||||
|
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
||||||
|
return location.origin
|
||||||
|
})
|
||||||
|
|
||||||
if (root instanceof HTMLElement) {
|
if (root instanceof HTMLElement) {
|
||||||
|
const server: ServerConnection.Http = { type: "http", http: { url: defaultUrl } }
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<PlatformProvider value={platform}>
|
<PlatformProvider value={platform}>
|
||||||
<AppBaseProviders>
|
<AppBaseProviders>
|
||||||
<AppInterface />
|
<AppInterface defaultServer={ServerConnection.key(server)} servers={[server]} />
|
||||||
</AppBaseProviders>
|
</AppBaseProviders>
|
||||||
</PlatformProvider>
|
</PlatformProvider>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق",
|
"command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق",
|
||||||
"command.model.variant.cycle": "تغيير جهد التفكير",
|
"command.model.variant.cycle": "تغيير جهد التفكير",
|
||||||
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
||||||
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
||||||
"command.workspace.toggle": "تبديل مساحات العمل",
|
"command.workspace.toggle": "تبديل مساحات العمل",
|
||||||
@@ -210,6 +212,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
||||||
"prompt.placeholder.summarizeComment": "لخّص التعليق…",
|
"prompt.placeholder.summarizeComment": "لخّص التعليق…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc للخروج",
|
"prompt.mode.shell.exit": "esc للخروج",
|
||||||
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
|
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
|
||||||
"prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",
|
"prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Mudar para o agente anterior",
|
"command.agent.cycle.reverse.description": "Mudar para o agente anterior",
|
||||||
"command.model.variant.cycle": "Alternar nível de raciocínio",
|
"command.model.variant.cycle": "Alternar nível de raciocínio",
|
||||||
"command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
|
"command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
|
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
|
||||||
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
|
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
|
||||||
"command.workspace.toggle": "Alternar espaços de trabalho",
|
"command.workspace.toggle": "Alternar espaços de trabalho",
|
||||||
@@ -210,6 +212,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
||||||
"prompt.placeholder.summarizeComment": "Resumir comentário…",
|
"prompt.placeholder.summarizeComment": "Resumir comentário…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc para sair",
|
"prompt.mode.shell.exit": "esc para sair",
|
||||||
"prompt.example.1": "Corrigir um TODO no código",
|
"prompt.example.1": "Corrigir um TODO no código",
|
||||||
"prompt.example.2": "Qual é a stack tecnológica deste projeto?",
|
"prompt.example.2": "Qual é a stack tecnológica deste projeto?",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta",
|
"command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta",
|
||||||
"command.model.variant.cycle": "Promijeni nivo razmišljanja",
|
"command.model.variant.cycle": "Promijeni nivo razmišljanja",
|
||||||
"command.model.variant.cycle.description": "Prebaci na sljedeći nivo",
|
"command.model.variant.cycle.description": "Prebaci na sljedeći nivo",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
|
"command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
|
||||||
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
|
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
|
||||||
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
|
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
|
||||||
@@ -228,6 +230,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
||||||
"prompt.placeholder.summarizeComment": "Sažmi komentar…",
|
"prompt.placeholder.summarizeComment": "Sažmi komentar…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc za izlaz",
|
"prompt.mode.shell.exit": "esc za izlaz",
|
||||||
|
|
||||||
"prompt.example.1": "Popravi TODO u bazi koda",
|
"prompt.example.1": "Popravi TODO u bazi koda",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Skift til forrige agent",
|
"command.agent.cycle.reverse.description": "Skift til forrige agent",
|
||||||
"command.model.variant.cycle": "Skift tænkeindsats",
|
"command.model.variant.cycle": "Skift tænkeindsats",
|
||||||
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
||||||
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
||||||
"command.workspace.toggle": "Skift arbejdsområder",
|
"command.workspace.toggle": "Skift arbejdsområder",
|
||||||
@@ -226,6 +228,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
||||||
"prompt.placeholder.summarizeComment": "Opsummér kommentar…",
|
"prompt.placeholder.summarizeComment": "Opsummér kommentar…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc for at afslutte",
|
"prompt.mode.shell.exit": "esc for at afslutte",
|
||||||
|
|
||||||
"prompt.example.1": "Ret en TODO i koden",
|
"prompt.example.1": "Ret en TODO i koden",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln",
|
"command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln",
|
||||||
"command.model.variant.cycle": "Denkaufwand wechseln",
|
"command.model.variant.cycle": "Denkaufwand wechseln",
|
||||||
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
||||||
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
||||||
"command.workspace.toggle": "Arbeitsbereiche umschalten",
|
"command.workspace.toggle": "Arbeitsbereiche umschalten",
|
||||||
@@ -215,6 +217,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
||||||
"prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
|
"prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc zum Verlassen",
|
"prompt.mode.shell.exit": "esc zum Verlassen",
|
||||||
"prompt.example.1": "Ein TODO in der Codebasis beheben",
|
"prompt.example.1": "Ein TODO in der Codebasis beheben",
|
||||||
"prompt.example.2": "Was ist der Tech-Stack dieses Projekts?",
|
"prompt.example.2": "Was ist der Tech-Stack dieses Projekts?",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
||||||
"command.model.variant.cycle": "Cycle thinking effort",
|
"command.model.variant.cycle": "Cycle thinking effort",
|
||||||
"command.model.variant.cycle.description": "Switch to the next effort level",
|
"command.model.variant.cycle.description": "Switch to the next effort level",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
||||||
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
|
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
|
||||||
"command.workspace.toggle": "Toggle workspaces",
|
"command.workspace.toggle": "Toggle workspaces",
|
||||||
@@ -228,6 +230,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
||||||
"prompt.placeholder.summarizeComment": "Summarize comment…",
|
"prompt.placeholder.summarizeComment": "Summarize comment…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc to exit",
|
"prompt.mode.shell.exit": "esc to exit",
|
||||||
|
|
||||||
"prompt.example.1": "Fix a TODO in the codebase",
|
"prompt.example.1": "Fix a TODO in the codebase",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Cambiar al agente anterior",
|
"command.agent.cycle.reverse.description": "Cambiar al agente anterior",
|
||||||
"command.model.variant.cycle": "Alternar esfuerzo de pensamiento",
|
"command.model.variant.cycle": "Alternar esfuerzo de pensamiento",
|
||||||
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
||||||
"command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente",
|
"command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente",
|
||||||
"command.workspace.toggle": "Alternar espacios de trabajo",
|
"command.workspace.toggle": "Alternar espacios de trabajo",
|
||||||
@@ -227,6 +229,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
||||||
"prompt.placeholder.summarizeComment": "Resumir comentario…",
|
"prompt.placeholder.summarizeComment": "Resumir comentario…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc para salir",
|
"prompt.mode.shell.exit": "esc para salir",
|
||||||
|
|
||||||
"prompt.example.1": "Arreglar un TODO en el código",
|
"prompt.example.1": "Arreglar un TODO en el código",
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Passer à l'agent précédent",
|
"command.agent.cycle.reverse.description": "Passer à l'agent précédent",
|
||||||
"command.model.variant.cycle": "Changer l'effort de réflexion",
|
"command.model.variant.cycle": "Changer l'effort de réflexion",
|
||||||
"command.model.variant.cycle.description": "Passer au niveau d'effort suivant",
|
"command.model.variant.cycle.description": "Passer au niveau d'effort suivant",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
||||||
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
|
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
|
||||||
"command.workspace.toggle": "Basculer les espaces de travail",
|
"command.workspace.toggle": "Basculer les espaces de travail",
|
||||||
@@ -210,6 +212,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
||||||
"prompt.placeholder.summarizeComment": "Résumer le commentaire…",
|
"prompt.placeholder.summarizeComment": "Résumer le commentaire…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc pour quitter",
|
"prompt.mode.shell.exit": "esc pour quitter",
|
||||||
"prompt.example.1": "Corriger un TODO dans la base de code",
|
"prompt.example.1": "Corriger un TODO dans la base de code",
|
||||||
"prompt.example.2": "Quelle est la pile technique de ce projet ?",
|
"prompt.example.2": "Quelle est la pile technique de ce projet ?",
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "前のエージェントに切り替え",
|
"command.agent.cycle.reverse.description": "前のエージェントに切り替え",
|
||||||
"command.model.variant.cycle": "思考レベルの切り替え",
|
"command.model.variant.cycle": "思考レベルの切り替え",
|
||||||
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
||||||
|
"command.prompt.mode.shell": "シェル",
|
||||||
|
"command.prompt.mode.normal": "プロンプト",
|
||||||
"command.permissions.autoaccept.enable": "編集を自動承認",
|
"command.permissions.autoaccept.enable": "編集を自動承認",
|
||||||
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
||||||
"command.workspace.toggle": "ワークスペースを切り替え",
|
"command.workspace.toggle": "ワークスペースを切り替え",
|
||||||
@@ -209,6 +211,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
||||||
"prompt.placeholder.summarizeComment": "コメントを要約…",
|
"prompt.placeholder.summarizeComment": "コメントを要約…",
|
||||||
"prompt.mode.shell": "シェル",
|
"prompt.mode.shell": "シェル",
|
||||||
|
"prompt.mode.normal": "プロンプト",
|
||||||
"prompt.mode.shell.exit": "escで終了",
|
"prompt.mode.shell.exit": "escで終了",
|
||||||
"prompt.example.1": "コードベースのTODOを修正",
|
"prompt.example.1": "コードベースのTODOを修正",
|
||||||
"prompt.example.2": "このプロジェクトの技術スタックは何ですか?",
|
"prompt.example.2": "このプロジェクトの技術スタックは何ですか?",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "이전 에이전트로 전환",
|
"command.agent.cycle.reverse.description": "이전 에이전트로 전환",
|
||||||
"command.model.variant.cycle": "생각 수준 순환",
|
"command.model.variant.cycle": "생각 수준 순환",
|
||||||
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
||||||
|
"command.prompt.mode.shell": "셸",
|
||||||
|
"command.prompt.mode.normal": "프롬프트",
|
||||||
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
||||||
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
||||||
"command.workspace.toggle": "작업 공간 전환",
|
"command.workspace.toggle": "작업 공간 전환",
|
||||||
@@ -213,6 +215,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
||||||
"prompt.placeholder.summarizeComment": "댓글 요약…",
|
"prompt.placeholder.summarizeComment": "댓글 요약…",
|
||||||
"prompt.mode.shell": "셸",
|
"prompt.mode.shell": "셸",
|
||||||
|
"prompt.mode.normal": "프롬프트",
|
||||||
"prompt.mode.shell.exit": "종료하려면 esc",
|
"prompt.mode.shell.exit": "종료하려면 esc",
|
||||||
"prompt.example.1": "코드베이스의 TODO 수정",
|
"prompt.example.1": "코드베이스의 TODO 수정",
|
||||||
"prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?",
|
"prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?",
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Bytt til forrige agent",
|
"command.agent.cycle.reverse.description": "Bytt til forrige agent",
|
||||||
"command.model.variant.cycle": "Bytt tenkeinnsats",
|
"command.model.variant.cycle": "Bytt tenkeinnsats",
|
||||||
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
||||||
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
||||||
"command.workspace.toggle": "Veksle arbeidsområder",
|
"command.workspace.toggle": "Veksle arbeidsområder",
|
||||||
@@ -230,6 +232,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
||||||
"prompt.placeholder.summarizeComment": "Oppsummer kommentar…",
|
"prompt.placeholder.summarizeComment": "Oppsummer kommentar…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "ESC for å avslutte",
|
"prompt.mode.shell.exit": "ESC for å avslutte",
|
||||||
|
|
||||||
"prompt.example.1": "Fiks en TODO i kodebasen",
|
"prompt.example.1": "Fiks en TODO i kodebasen",
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta",
|
"command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta",
|
||||||
"command.model.variant.cycle": "Przełącz wysiłek myślowy",
|
"command.model.variant.cycle": "Przełącz wysiłek myślowy",
|
||||||
"command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
|
"command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
|
||||||
|
"command.prompt.mode.shell": "Terminal",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
|
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
|
||||||
"command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji",
|
"command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji",
|
||||||
"command.workspace.toggle": "Przełącz przestrzenie robocze",
|
"command.workspace.toggle": "Przełącz przestrzenie robocze",
|
||||||
@@ -211,6 +213,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
||||||
"prompt.placeholder.summarizeComment": "Podsumuj komentarz…",
|
"prompt.placeholder.summarizeComment": "Podsumuj komentarz…",
|
||||||
"prompt.mode.shell": "Terminal",
|
"prompt.mode.shell": "Terminal",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc aby wyjść",
|
"prompt.mode.shell.exit": "esc aby wyjść",
|
||||||
"prompt.example.1": "Napraw TODO w bazie kodu",
|
"prompt.example.1": "Napraw TODO w bazie kodu",
|
||||||
"prompt.example.2": "Jaki jest stos technologiczny tego projektu?",
|
"prompt.example.2": "Jaki jest stos technologiczny tego projektu?",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту",
|
"command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту",
|
||||||
"command.model.variant.cycle": "Цикл режимов мышления",
|
"command.model.variant.cycle": "Цикл режимов мышления",
|
||||||
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
||||||
|
"command.prompt.mode.shell": "Оболочка",
|
||||||
|
"command.prompt.mode.normal": "Промпт",
|
||||||
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
||||||
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
||||||
"command.workspace.toggle": "Переключить рабочие пространства",
|
"command.workspace.toggle": "Переключить рабочие пространства",
|
||||||
@@ -227,6 +229,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
||||||
"prompt.placeholder.summarizeComment": "Суммировать комментарий…",
|
"prompt.placeholder.summarizeComment": "Суммировать комментарий…",
|
||||||
"prompt.mode.shell": "Оболочка",
|
"prompt.mode.shell": "Оболочка",
|
||||||
|
"prompt.mode.normal": "Промпт",
|
||||||
"prompt.mode.shell.exit": "esc для выхода",
|
"prompt.mode.shell.exit": "esc для выхода",
|
||||||
|
|
||||||
"prompt.example.1": "Исправить TODO в коде",
|
"prompt.example.1": "Исправить TODO в коде",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า",
|
"command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า",
|
||||||
"command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด",
|
"command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด",
|
||||||
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
||||||
|
"command.prompt.mode.shell": "เชลล์",
|
||||||
|
"command.prompt.mode.normal": "พรอมต์",
|
||||||
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||||
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||||
"command.workspace.toggle": "สลับพื้นที่ทำงาน",
|
"command.workspace.toggle": "สลับพื้นที่ทำงาน",
|
||||||
@@ -227,6 +229,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
||||||
"prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…",
|
"prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…",
|
||||||
"prompt.mode.shell": "เชลล์",
|
"prompt.mode.shell": "เชลล์",
|
||||||
|
"prompt.mode.normal": "พรอมต์",
|
||||||
"prompt.mode.shell.exit": "กด esc เพื่อออก",
|
"prompt.mode.shell.exit": "กด esc เพื่อออก",
|
||||||
|
|
||||||
"prompt.example.1": "แก้ไข TODO ในโค้ดเบส",
|
"prompt.example.1": "แก้ไข TODO ในโค้ดเบส",
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ export const dict = {
|
|||||||
"command.model.variant.cycle": "切换思考强度",
|
"command.model.variant.cycle": "切换思考强度",
|
||||||
"command.model.variant.cycle.description": "切换到下一个强度等级",
|
"command.model.variant.cycle.description": "切换到下一个强度等级",
|
||||||
|
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
|
|
||||||
"command.permissions.autoaccept.enable": "自动接受编辑",
|
"command.permissions.autoaccept.enable": "自动接受编辑",
|
||||||
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
||||||
|
|
||||||
@@ -248,6 +251,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "总结评论…",
|
"prompt.placeholder.summarizeComments": "总结评论…",
|
||||||
"prompt.placeholder.summarizeComment": "总结该评论…",
|
"prompt.placeholder.summarizeComment": "总结该评论…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "按 esc 退出",
|
"prompt.mode.shell.exit": "按 esc 退出",
|
||||||
"prompt.example.1": "修复代码库中的一个 TODO",
|
"prompt.example.1": "修复代码库中的一个 TODO",
|
||||||
"prompt.example.2": "这个项目的技术栈是什么?",
|
"prompt.example.2": "这个项目的技术栈是什么?",
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "切換到上一個代理程式",
|
"command.agent.cycle.reverse.description": "切換到上一個代理程式",
|
||||||
"command.model.variant.cycle": "循環思考強度",
|
"command.model.variant.cycle": "循環思考強度",
|
||||||
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
||||||
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
"command.prompt.mode.normal": "Prompt",
|
||||||
"command.permissions.autoaccept.enable": "自動接受編輯",
|
"command.permissions.autoaccept.enable": "自動接受編輯",
|
||||||
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
||||||
"command.workspace.toggle": "切換工作區",
|
"command.workspace.toggle": "切換工作區",
|
||||||
@@ -227,6 +229,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "摘要評論…",
|
"prompt.placeholder.summarizeComments": "摘要評論…",
|
||||||
"prompt.placeholder.summarizeComment": "摘要這則評論…",
|
"prompt.placeholder.summarizeComment": "摘要這則評論…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "按 esc 退出",
|
"prompt.mode.shell.exit": "按 esc 退出",
|
||||||
|
|
||||||
"prompt.example.1": "修復程式碼庫中的一個 TODO",
|
"prompt.example.1": "修復程式碼庫中的一個 TODO",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { PlatformProvider, type Platform, type DisplayBackend } from "./context/platform"
|
|
||||||
export { AppBaseProviders, AppInterface } from "./app"
|
export { AppBaseProviders, AppInterface } from "./app"
|
||||||
export { useCommand } from "./context/command"
|
export { useCommand } from "./context/command"
|
||||||
|
export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform"
|
||||||
|
export { ServerConnection } from "./context/server"
|
||||||
export { handleNotificationClick } from "./utils/notification-click"
|
export { handleNotificationClick } from "./utils/notification-click"
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ import { DialogSelectServer } from "@/components/dialog-select-server"
|
|||||||
import { DialogSettings } from "@/components/dialog-settings"
|
import { DialogSettings } from "@/components/dialog-settings"
|
||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
||||||
import { navStart } from "@/utils/perf"
|
|
||||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||||
import { DialogEditProject } from "@/components/dialog-edit-project"
|
import { DialogEditProject } from "@/components/dialog-edit-project"
|
||||||
import { Titlebar } from "@/components/titlebar"
|
import { Titlebar } from "@/components/titlebar"
|
||||||
@@ -178,7 +177,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
|
const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
|
||||||
const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering())
|
const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering())
|
||||||
const clearHoverProjectSoon = () => queueMicrotask(() => setState("hoverProject", undefined))
|
const setHoverProject = (value: string | undefined) => {
|
||||||
|
setState("hoverProject", value)
|
||||||
|
if (value !== undefined) return
|
||||||
|
aim.reset()
|
||||||
|
}
|
||||||
|
const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined))
|
||||||
const setHoverSession = (id: string | undefined) => setState("hoverSession", id)
|
const setHoverSession = (id: string | undefined) => setState("hoverSession", id)
|
||||||
|
|
||||||
const hoverProjectData = createMemo(() => {
|
const hoverProjectData = createMemo(() => {
|
||||||
@@ -189,13 +193,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!layout.sidebar.opened()) return
|
if (!layout.sidebar.opened()) return
|
||||||
aim.reset()
|
setHoverProject(undefined)
|
||||||
setState("hoverProject", undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (state.hoverProject !== undefined) return
|
|
||||||
aim.reset()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const autoselecting = createMemo(() => {
|
const autoselecting = createMemo(() => {
|
||||||
@@ -226,7 +224,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const clearSidebarHoverState = () => {
|
const clearSidebarHoverState = () => {
|
||||||
if (layout.sidebar.opened()) return
|
if (layout.sidebar.opened()) return
|
||||||
setState("hoverSession", undefined)
|
setState("hoverSession", undefined)
|
||||||
setState("hoverProject", undefined)
|
setHoverProject(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigateWithSidebarReset = (href: string) => {
|
const navigateWithSidebarReset = (href: string) => {
|
||||||
@@ -826,14 +824,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (next) prefetchSession(next)
|
if (next) prefetchSession(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
navStart({
|
|
||||||
dir: base64Encode(session.directory),
|
|
||||||
from: params.id,
|
|
||||||
to: session.id,
|
|
||||||
trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
navigateToSession(session)
|
navigateToSession(session)
|
||||||
queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
|
queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
|
||||||
}
|
}
|
||||||
@@ -869,15 +859,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (next) prefetchSession(next)
|
if (next) prefetchSession(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
navStart({
|
|
||||||
dir: base64Encode(session.directory),
|
|
||||||
from: params.id,
|
|
||||||
to: session.id,
|
|
||||||
trigger: offset > 0 ? "shift+alt+arrowdown" : "shift+alt+arrowup",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToSession(session)
|
navigateToSession(session)
|
||||||
queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
|
queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
|
||||||
return
|
return
|
||||||
@@ -1508,7 +1489,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
function handleDragStart(event: unknown) {
|
function handleDragStart(event: unknown) {
|
||||||
const id = getDraggableId(event)
|
const id = getDraggableId(event)
|
||||||
if (!id) return
|
if (!id) return
|
||||||
setState("hoverProject", undefined)
|
setHoverProject(undefined)
|
||||||
setStore("activeProject", id)
|
setStore("activeProject", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1942,7 +1923,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
||||||
navLeave.current = window.setTimeout(() => {
|
navLeave.current = window.setTimeout(() => {
|
||||||
navLeave.current = undefined
|
navLeave.current = undefined
|
||||||
setState("hoverProject", undefined)
|
setHoverProject(undefined)
|
||||||
setState("hoverSession", undefined)
|
setState("hoverSession", undefined)
|
||||||
}, 300)
|
}, 300)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,57 +1,30 @@
|
|||||||
import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
|
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount } from "solid-js"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { Dynamic } from "solid-js/web"
|
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
|
||||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
|
|
||||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
|
||||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
|
||||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { checksum, base64Encode } from "@opencode-ai/util/encode"
|
import { checksum, base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { findLast } from "@opencode-ai/util/array"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
|
||||||
import FileTree from "@/components/file-tree"
|
|
||||||
import { useCommand } from "@/context/command"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
import { SessionHeader, NewSessionView } from "@/components/session"
|
||||||
import { usePermission } from "@/context/permission"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { SessionHeader, SessionContextTab, SortableTab, FileVisual, NewSessionView } from "@/components/session"
|
|
||||||
import { navMark, navParams } from "@/utils/perf"
|
|
||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
import { createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "@/pages/session/helpers"
|
import { createOpenReviewFile } from "@/pages/session/helpers"
|
||||||
import { createScrollSpy } from "@/pages/session/scroll-spy"
|
import { createScrollSpy } from "@/pages/session/scroll-spy"
|
||||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
import { SessionReviewTab, type DiffStyle, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
|
||||||
import {
|
|
||||||
SessionReviewTab,
|
|
||||||
StickyAddButton,
|
|
||||||
type DiffStyle,
|
|
||||||
type SessionReviewTabProps,
|
|
||||||
} from "@/pages/session/review-tab"
|
|
||||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||||
import { terminalTabLabel } from "@/pages/session/terminal-label"
|
|
||||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||||
import { SessionPromptDock } from "@/pages/session/session-prompt-dock"
|
import { SessionPromptDock } from "@/pages/session/session-prompt-dock"
|
||||||
@@ -59,98 +32,34 @@ import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
|
|||||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||||
|
|
||||||
type HandoffSession = {
|
|
||||||
prompt: string
|
|
||||||
files: Record<string, SelectedLineRange | null>
|
|
||||||
}
|
|
||||||
|
|
||||||
const HANDOFF_MAX = 40
|
|
||||||
|
|
||||||
const handoff = {
|
|
||||||
session: new Map<string, HandoffSession>(),
|
|
||||||
terminal: new Map<string, string[]>(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const touch = <K, V>(map: Map<K, V>, key: K, value: V) => {
|
|
||||||
map.delete(key)
|
|
||||||
map.set(key, value)
|
|
||||||
while (map.size > HANDOFF_MAX) {
|
|
||||||
const first = map.keys().next().value
|
|
||||||
if (first === undefined) return
|
|
||||||
map.delete(first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setSessionHandoff = (key: string, patch: Partial<HandoffSession>) => {
|
|
||||||
const prev = handoff.session.get(key) ?? { prompt: "", files: {} }
|
|
||||||
touch(handoff.session, key, { ...prev, ...patch })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const globalSync = useGlobalSync()
|
|
||||||
const terminal = useTerminal()
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const codeComponent = useCodeComponent()
|
|
||||||
const command = useCommand()
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
const permission = usePermission()
|
|
||||||
|
|
||||||
const permRequest = createMemo(() => {
|
|
||||||
const sessionID = params.id
|
|
||||||
if (!sessionID) return
|
|
||||||
return sync.data.permission[sessionID]?.[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
const questionRequest = createMemo(() => {
|
|
||||||
const sessionID = params.id
|
|
||||||
if (!sessionID) return
|
|
||||||
return sync.data.question[sessionID]?.[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
const blocked = createMemo(() => !!permRequest() || !!questionRequest())
|
|
||||||
|
|
||||||
const [ui, setUi] = createStore({
|
const [ui, setUi] = createStore({
|
||||||
responding: false,
|
|
||||||
pendingMessage: undefined as string | undefined,
|
pendingMessage: undefined as string | undefined,
|
||||||
scrollGesture: 0,
|
scrollGesture: 0,
|
||||||
autoCreated: false,
|
|
||||||
scroll: {
|
scroll: {
|
||||||
overflow: false,
|
overflow: false,
|
||||||
bottom: true,
|
bottom: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
const blocked = createMemo(() => {
|
||||||
on(
|
const sessionID = params.id
|
||||||
() => permRequest()?.id,
|
if (!sessionID) return false
|
||||||
() => setUi("responding", false),
|
return !!sync.data.permission[sessionID]?.[0] || !!sync.data.question[sessionID]?.[0]
|
||||||
{ defer: true },
|
})
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const decide = (response: "once" | "always" | "reject") => {
|
|
||||||
const perm = permRequest()
|
|
||||||
if (!perm) return
|
|
||||||
if (ui.responding) return
|
|
||||||
|
|
||||||
setUi("responding", true)
|
|
||||||
sdk.client.permission
|
|
||||||
.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
|
||||||
})
|
|
||||||
.finally(() => setUi("responding", false))
|
|
||||||
}
|
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const workspaceKey = createMemo(() => params.dir ?? "")
|
const workspaceKey = createMemo(() => params.dir ?? "")
|
||||||
const workspaceTabs = createMemo(() => layout.tabs(workspaceKey))
|
const workspaceTabs = createMemo(() => layout.tabs(workspaceKey))
|
||||||
@@ -193,46 +102,6 @@ export default function Page() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => [params.dir, params.id] as const,
|
|
||||||
([dir, id], prev) => {
|
|
||||||
if (!id) return
|
|
||||||
navParams({ dir, from: prev?.[1], to: id })
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
if (!prompt.ready()) return
|
|
||||||
navMark({ dir: params.dir, to: id, name: "storage:prompt-ready" })
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
if (!terminal.ready()) return
|
|
||||||
navMark({ dir: params.dir, to: id, name: "storage:terminal-ready" })
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
if (!file.ready()) return
|
|
||||||
navMark({ dir: params.dir, to: id, name: "storage:file-view-ready" })
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
if (sync.data.message[id] === undefined) return
|
|
||||||
navMark({ dir: params.dir, to: id, name: "session:data-ready" })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
|
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
|
||||||
@@ -265,16 +134,6 @@ export default function Page() {
|
|||||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
const openTab = (value: string) => {
|
|
||||||
const next = normalizeTab(value)
|
|
||||||
tabs().open(next)
|
|
||||||
|
|
||||||
const path = file.pathFromTab(next)
|
|
||||||
if (!path) return
|
|
||||||
file.load(path)
|
|
||||||
openReviewPanel()
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const active = tabs().active()
|
const active = tabs().active()
|
||||||
if (!active) return
|
if (!active) return
|
||||||
@@ -323,206 +182,6 @@ export default function Page() {
|
|||||||
return sync.session.history.loading(id)
|
return sync.session.history.loading(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const [title, setTitle] = createStore({
|
|
||||||
draft: "",
|
|
||||||
editing: false,
|
|
||||||
saving: false,
|
|
||||||
menuOpen: false,
|
|
||||||
pendingRename: false,
|
|
||||||
})
|
|
||||||
let titleRef: HTMLInputElement | undefined
|
|
||||||
|
|
||||||
const errorMessage = (err: unknown) => {
|
|
||||||
if (err && typeof err === "object" && "data" in err) {
|
|
||||||
const data = (err as { data?: { message?: string } }).data
|
|
||||||
if (data?.message) return data.message
|
|
||||||
}
|
|
||||||
if (err instanceof Error) return err.message
|
|
||||||
return language.t("common.requestFailed")
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
sessionKey,
|
|
||||||
() => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const openTitleEditor = () => {
|
|
||||||
if (!params.id) return
|
|
||||||
setTitle({ editing: true, draft: info()?.title ?? "" })
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
titleRef?.focus()
|
|
||||||
titleRef?.select()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeTitleEditor = () => {
|
|
||||||
if (title.saving) return
|
|
||||||
setTitle({ editing: false, saving: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveTitleEditor = async () => {
|
|
||||||
const sessionID = params.id
|
|
||||||
if (!sessionID) return
|
|
||||||
if (title.saving) return
|
|
||||||
|
|
||||||
const next = title.draft.trim()
|
|
||||||
if (!next || next === (info()?.title ?? "")) {
|
|
||||||
setTitle({ editing: false, saving: false })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setTitle("saving", true)
|
|
||||||
await sdk.client.session
|
|
||||||
.update({ sessionID, title: next })
|
|
||||||
.then(() => {
|
|
||||||
sync.set(
|
|
||||||
produce((draft) => {
|
|
||||||
const index = draft.session.findIndex((s) => s.id === sessionID)
|
|
||||||
if (index !== -1) draft.session[index].title = next
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
setTitle({ editing: false, saving: false })
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setTitle("saving", false)
|
|
||||||
showToast({
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: errorMessage(err),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
|
||||||
if (params.id !== sessionID) return
|
|
||||||
if (parentID) {
|
|
||||||
navigate(`/${params.dir}/session/${parentID}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (nextSessionID) {
|
|
||||||
navigate(`/${params.dir}/session/${nextSessionID}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
navigate(`/${params.dir}/session`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function archiveSession(sessionID: string) {
|
|
||||||
const session = sync.session.get(sessionID)
|
|
||||||
if (!session) return
|
|
||||||
|
|
||||||
const sessions = sync.data.session ?? []
|
|
||||||
const index = sessions.findIndex((s) => s.id === sessionID)
|
|
||||||
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
|
||||||
|
|
||||||
await sdk.client.session
|
|
||||||
.update({ sessionID, time: { archived: Date.now() } })
|
|
||||||
.then(() => {
|
|
||||||
sync.set(
|
|
||||||
produce((draft) => {
|
|
||||||
const index = draft.session.findIndex((s) => s.id === sessionID)
|
|
||||||
if (index !== -1) draft.session.splice(index, 1)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
showToast({
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: errorMessage(err),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteSession(sessionID: string) {
|
|
||||||
const session = sync.session.get(sessionID)
|
|
||||||
if (!session) return false
|
|
||||||
|
|
||||||
const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
|
|
||||||
const index = sessions.findIndex((s) => s.id === sessionID)
|
|
||||||
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
|
||||||
|
|
||||||
const result = await sdk.client.session
|
|
||||||
.delete({ sessionID })
|
|
||||||
.then((x) => x.data)
|
|
||||||
.catch((err) => {
|
|
||||||
showToast({
|
|
||||||
title: language.t("session.delete.failed.title"),
|
|
||||||
description: errorMessage(err),
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!result) return false
|
|
||||||
|
|
||||||
sync.set(
|
|
||||||
produce((draft) => {
|
|
||||||
const removed = new Set<string>([sessionID])
|
|
||||||
|
|
||||||
const byParent = new Map<string, string[]>()
|
|
||||||
for (const item of draft.session) {
|
|
||||||
const parentID = item.parentID
|
|
||||||
if (!parentID) continue
|
|
||||||
const existing = byParent.get(parentID)
|
|
||||||
if (existing) {
|
|
||||||
existing.push(item.id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
byParent.set(parentID, [item.id])
|
|
||||||
}
|
|
||||||
|
|
||||||
const stack = [sessionID]
|
|
||||||
while (stack.length) {
|
|
||||||
const parentID = stack.pop()
|
|
||||||
if (!parentID) continue
|
|
||||||
|
|
||||||
const children = byParent.get(parentID)
|
|
||||||
if (!children) continue
|
|
||||||
|
|
||||||
for (const child of children) {
|
|
||||||
if (removed.has(child)) continue
|
|
||||||
removed.add(child)
|
|
||||||
stack.push(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDeleteSession(props: { sessionID: string }) {
|
|
||||||
const title = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new"))
|
|
||||||
const handleDelete = async () => {
|
|
||||||
await deleteSession(props.sessionID)
|
|
||||||
dialog.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog title={language.t("session.delete.title")} fit>
|
|
||||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<span class="text-14-regular text-text-strong">
|
|
||||||
{language.t("session.delete.confirm", { name: title() })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" size="large" onClick={handleDelete}>
|
|
||||||
{language.t("session.delete.button")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyUserMessages: UserMessage[] = []
|
const emptyUserMessages: UserMessage[] = []
|
||||||
const userMessages = createMemo(
|
const userMessages = createMemo(
|
||||||
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||||
@@ -555,8 +214,6 @@ export default function Page() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
activeDraggable: undefined as string | undefined,
|
|
||||||
activeTerminalDraggable: undefined as string | undefined,
|
|
||||||
messageId: undefined as string | undefined,
|
messageId: undefined as string | undefined,
|
||||||
turnStart: 0,
|
turnStart: 0,
|
||||||
mobileTab: "session" as "session" | "changes",
|
mobileTab: "session" as "session" | "changes",
|
||||||
@@ -615,33 +272,6 @@ export default function Page() {
|
|||||||
scrollToMessage(msgs[targetIndex], "auto")
|
scrollToMessage(msgs[targetIndex], "auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
const kinds = createMemo(() => {
|
|
||||||
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
|
||||||
if (!a) return b
|
|
||||||
if (a === b) return a
|
|
||||||
return "mix" as const
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
|
||||||
|
|
||||||
const out = new Map<string, "add" | "del" | "mix">()
|
|
||||||
for (const diff of diffs()) {
|
|
||||||
const file = normalize(diff.file)
|
|
||||||
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
|
||||||
|
|
||||||
out.set(file, kind)
|
|
||||||
|
|
||||||
const parts = file.split("/")
|
|
||||||
for (const [idx] of parts.slice(0, -1).entries()) {
|
|
||||||
const dir = parts.slice(0, idx + 1).join("/")
|
|
||||||
if (!dir) continue
|
|
||||||
out.set(dir, merge(out.get(dir), kind))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
const emptyDiffFiles: string[] = []
|
|
||||||
const diffFiles = createMemo(() => diffs().map((d) => d.file), emptyDiffFiles, { equals: same })
|
|
||||||
const diffsReady = createMemo(() => {
|
const diffsReady = createMemo(() => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return true
|
if (!id) return true
|
||||||
@@ -649,7 +279,6 @@ export default function Page() {
|
|||||||
return sync.data.session_diff[id] !== undefined
|
return sync.data.session_diff[id] !== undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const idle = { type: "idle" as const }
|
|
||||||
let inputRef!: HTMLDivElement
|
let inputRef!: HTMLDivElement
|
||||||
let promptDock: HTMLDivElement | undefined
|
let promptDock: HTMLDivElement | undefined
|
||||||
let dockHeight = 0
|
let dockHeight = 0
|
||||||
@@ -679,43 +308,6 @@ export default function Page() {
|
|||||||
void sync.session.todo(id)
|
void sync.session.todo(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!view().terminal.opened()) {
|
|
||||||
setUi("autoCreated", false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!terminal.ready() || terminal.all().length !== 0 || ui.autoCreated) return
|
|
||||||
terminal.new()
|
|
||||||
setUi("autoCreated", true)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => terminal.all().length,
|
|
||||||
(count, prevCount) => {
|
|
||||||
if (prevCount !== undefined && prevCount > 0 && count === 0) {
|
|
||||||
if (view().terminal.opened()) {
|
|
||||||
view().terminal.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => terminal.active(),
|
|
||||||
(activeId) => {
|
|
||||||
if (!activeId || !view().terminal.opened()) return
|
|
||||||
// Immediately remove focus
|
|
||||||
if (document.activeElement instanceof HTMLElement) {
|
|
||||||
document.activeElement.blur()
|
|
||||||
}
|
|
||||||
focusTerminalById(activeId)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => visibleUserMessages().at(-1)?.id,
|
() => visibleUserMessages().at(-1)?.id,
|
||||||
@@ -728,20 +320,12 @@ export default function Page() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
|
||||||
const todos = createMemo(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return []
|
|
||||||
return globalSync.data.session_todo[id] ?? []
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
() => {
|
() => {
|
||||||
setStore("messageId", undefined)
|
setStore("messageId", undefined)
|
||||||
setStore("changes", "session")
|
setStore("changes", "session")
|
||||||
setUi("autoCreated", false)
|
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
@@ -768,11 +352,6 @@ export default function Page() {
|
|||||||
return lines.slice(0, 2).join("\n")
|
return lines.slice(0, 2).join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSelectionToContext = (path: string, selection: FileSelection) => {
|
|
||||||
const preview = selectionPreview(path, selection)
|
|
||||||
prompt.context.add({ type: "file", path, selection, preview })
|
|
||||||
}
|
|
||||||
|
|
||||||
const addCommentToContext = (input: {
|
const addCommentToContext = (input: {
|
||||||
file: string
|
file: string
|
||||||
selection: SelectedLineRange
|
selection: SelectedLineRange
|
||||||
@@ -827,53 +406,6 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDragStart = (event: unknown) => {
|
|
||||||
const id = getDraggableId(event)
|
|
||||||
if (!id) return
|
|
||||||
setStore("activeDraggable", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragOver = (event: DragEvent) => {
|
|
||||||
const { draggable, droppable } = event
|
|
||||||
if (draggable && droppable) {
|
|
||||||
const currentTabs = tabs().all()
|
|
||||||
const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString())
|
|
||||||
if (toIndex === undefined) return
|
|
||||||
tabs().move(draggable.id.toString(), toIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
|
||||||
setStore("activeDraggable", undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTerminalDragStart = (event: unknown) => {
|
|
||||||
const id = getDraggableId(event)
|
|
||||||
if (!id) return
|
|
||||||
setStore("activeTerminalDraggable", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTerminalDragOver = (event: DragEvent) => {
|
|
||||||
const { draggable, droppable } = event
|
|
||||||
if (draggable && droppable) {
|
|
||||||
const terminals = terminal.all()
|
|
||||||
const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString())
|
|
||||||
const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString())
|
|
||||||
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
|
||||||
terminal.move(draggable.id.toString(), toIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTerminalDragEnd = () => {
|
|
||||||
setStore("activeTerminalDraggable", undefined)
|
|
||||||
const activeId = terminal.active()
|
|
||||||
if (!activeId) return
|
|
||||||
setTimeout(() => {
|
|
||||||
focusTerminalById(activeId)
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
|
const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
|
||||||
const openedTabs = createMemo(() =>
|
const openedTabs = createMemo(() =>
|
||||||
tabs()
|
tabs()
|
||||||
@@ -911,29 +443,8 @@ export default function Page() {
|
|||||||
const focusInput = () => inputRef?.focus()
|
const focusInput = () => inputRef?.focus()
|
||||||
|
|
||||||
useSessionCommands({
|
useSessionCommands({
|
||||||
command,
|
|
||||||
dialog,
|
|
||||||
file,
|
|
||||||
language,
|
|
||||||
local,
|
|
||||||
permission,
|
|
||||||
prompt,
|
|
||||||
sdk,
|
|
||||||
sync,
|
|
||||||
terminal,
|
|
||||||
layout,
|
|
||||||
params,
|
|
||||||
navigate,
|
|
||||||
tabs,
|
|
||||||
view,
|
|
||||||
info,
|
|
||||||
status,
|
|
||||||
userMessages,
|
|
||||||
visibleUserMessages,
|
|
||||||
showAllFiles,
|
|
||||||
navigateMessageByOffset,
|
navigateMessageByOffset,
|
||||||
setActiveMessage,
|
setActiveMessage,
|
||||||
addSelectionToContext,
|
|
||||||
focusInput,
|
focusInput,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1071,11 +582,6 @@ export default function Page() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const setFileTreeTabValue = (value: string) => {
|
|
||||||
if (value !== "changes" && value !== "all") return
|
|
||||||
setFileTreeTab(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const reviewDiffId = (path: string) => {
|
const reviewDiffId = (path: string) => {
|
||||||
const sum = checksum(path)
|
const sum = checksum(path)
|
||||||
if (!sum) return
|
if (!sum) return
|
||||||
@@ -1171,12 +677,6 @@ export default function Page() {
|
|||||||
return "empty"
|
return "empty"
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeFileTab = createMemo(() => {
|
|
||||||
const active = activeTab()
|
|
||||||
if (!openedTabs().includes(active)) return
|
|
||||||
return active
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!layout.ready()) return
|
if (!layout.ready()) return
|
||||||
if (tabs().active()) return
|
if (tabs().active()) return
|
||||||
@@ -1481,62 +981,10 @@ export default function Page() {
|
|||||||
consumePendingMessage: layout.pendingMessage.consume,
|
consumePendingMessage: layout.pendingMessage.consume,
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
onMount(() => {
|
||||||
document.addEventListener("keydown", handleKeyDown)
|
document.addEventListener("keydown", handleKeyDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
const previewPrompt = () =>
|
|
||||||
prompt
|
|
||||||
.current()
|
|
||||||
.map((part) => {
|
|
||||||
if (part.type === "file") return `[file:${part.path}]`
|
|
||||||
if (part.type === "agent") return `@${part.name}`
|
|
||||||
if (part.type === "image") return `[image:${part.filename}]`
|
|
||||||
return part.content
|
|
||||||
})
|
|
||||||
.join("")
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!prompt.ready()) return
|
|
||||||
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!terminal.ready()) return
|
|
||||||
language.locale()
|
|
||||||
|
|
||||||
touch(
|
|
||||||
handoff.terminal,
|
|
||||||
params.dir!,
|
|
||||||
terminal.all().map((pty) =>
|
|
||||||
terminalTabLabel({
|
|
||||||
title: pty.title,
|
|
||||||
titleNumber: pty.titleNumber,
|
|
||||||
t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!file.ready()) return
|
|
||||||
setSessionHandoff(sessionKey(), {
|
|
||||||
files: tabs()
|
|
||||||
.all()
|
|
||||||
.reduce<Record<string, SelectedLineRange | null>>((acc, tab) => {
|
|
||||||
const path = file.pathFromTab(tab)
|
|
||||||
if (!path) return acc
|
|
||||||
const selected = file.selectedLines(path)
|
|
||||||
acc[path] =
|
|
||||||
selected && typeof selected === "object" && "start" in selected && "end" in selected
|
|
||||||
? (selected as SelectedLineRange)
|
|
||||||
: null
|
|
||||||
return acc
|
|
||||||
}, {}),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
cancelTurnBackfill()
|
cancelTurnBackfill()
|
||||||
document.removeEventListener("keydown", handleKeyDown)
|
document.removeEventListener("keydown", handleKeyDown)
|
||||||
@@ -1555,7 +1003,6 @@ export default function Page() {
|
|||||||
reviewCount={reviewCount()}
|
reviewCount={reviewCount()}
|
||||||
onSession={() => setStore("mobileTab", "session")}
|
onSession={() => setStore("mobileTab", "session")}
|
||||||
onChanges={() => setStore("mobileTab", "changes")}
|
onChanges={() => setStore("mobileTab", "changes")}
|
||||||
t={language.t as (key: string, vars?: Record<string, string | number | boolean>) => string}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Session panel */}
|
{/* Session panel */}
|
||||||
@@ -1595,27 +1042,7 @@ export default function Page() {
|
|||||||
isDesktop={isDesktop()}
|
isDesktop={isDesktop()}
|
||||||
onScrollSpyScroll={scrollSpy.onScroll}
|
onScrollSpyScroll={scrollSpy.onScroll}
|
||||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||||
showHeader={!!(info()?.title || info()?.parentID)}
|
|
||||||
centered={centered()}
|
centered={centered()}
|
||||||
title={info()?.title}
|
|
||||||
parentID={info()?.parentID}
|
|
||||||
openTitleEditor={openTitleEditor}
|
|
||||||
closeTitleEditor={closeTitleEditor}
|
|
||||||
saveTitleEditor={saveTitleEditor}
|
|
||||||
titleRef={(el) => {
|
|
||||||
titleRef = el
|
|
||||||
}}
|
|
||||||
titleState={title}
|
|
||||||
onTitleDraft={(value) => setTitle("draft", value)}
|
|
||||||
onTitleMenuOpen={(open) => setTitle("menuOpen", open)}
|
|
||||||
onTitlePendingRename={(value) => setTitle("pendingRename", value)}
|
|
||||||
onNavigateParent={() => {
|
|
||||||
navigate(`/${params.dir}/session/${info()?.parentID}`)
|
|
||||||
}}
|
|
||||||
sessionID={params.id!}
|
|
||||||
onArchiveSession={(sessionID) => void archiveSession(sessionID)}
|
|
||||||
onDeleteSession={(sessionID) => dialog.show(() => <DialogDeleteSession sessionID={sessionID} />)}
|
|
||||||
t={language.t as (key: string, vars?: Record<string, string | number | boolean>) => string}
|
|
||||||
setContentRef={(el) => {
|
setContentRef={(el) => {
|
||||||
content = el
|
content = el
|
||||||
autoScroll.contentRef(el)
|
autoScroll.contentRef(el)
|
||||||
@@ -1637,11 +1064,6 @@ export default function Page() {
|
|||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
onRegisterMessage={scrollSpy.register}
|
onRegisterMessage={scrollSpy.register}
|
||||||
onUnregisterMessage={scrollSpy.unregister}
|
onUnregisterMessage={scrollSpy.unregister}
|
||||||
onFirstTurnMount={() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
navMark({ dir: params.dir, to: id, name: "session:first-turn-mounted" })
|
|
||||||
}}
|
|
||||||
lastUserMessageID={lastUserMessage()?.id}
|
lastUserMessageID={lastUserMessage()?.id}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -1670,15 +1092,6 @@ export default function Page() {
|
|||||||
|
|
||||||
<SessionPromptDock
|
<SessionPromptDock
|
||||||
centered={centered()}
|
centered={centered()}
|
||||||
questionRequest={questionRequest}
|
|
||||||
permissionRequest={permRequest}
|
|
||||||
blocked={blocked()}
|
|
||||||
todos={todos()}
|
|
||||||
promptReady={prompt.ready()}
|
|
||||||
handoffPrompt={handoff.session.get(sessionKey())?.prompt}
|
|
||||||
t={language.t as (key: string, vars?: Record<string, string | number | boolean>) => string}
|
|
||||||
responding={ui.responding}
|
|
||||||
onDecide={decide}
|
|
||||||
inputRef={(el) => {
|
inputRef={(el) => {
|
||||||
inputRef = el
|
inputRef = el
|
||||||
}}
|
}}
|
||||||
@@ -1688,7 +1101,9 @@ export default function Page() {
|
|||||||
comments.clear()
|
comments.clear()
|
||||||
resumeScroll()
|
resumeScroll()
|
||||||
}}
|
}}
|
||||||
setPromptDockRef={(el) => (promptDock = el)}
|
setPromptDockRef={(el) => {
|
||||||
|
promptDock = el
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Show when={desktopReviewOpen()}>
|
<Show when={desktopReviewOpen()}>
|
||||||
@@ -1702,64 +1117,10 @@ export default function Page() {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SessionSidePanel
|
<SessionSidePanel reviewPanel={reviewPanel} activeDiff={tree.activeDiff} focusReviewDiff={focusReviewDiff} />
|
||||||
open={desktopSidePanelOpen()}
|
|
||||||
reviewOpen={desktopReviewOpen()}
|
|
||||||
language={language}
|
|
||||||
layout={layout}
|
|
||||||
command={command}
|
|
||||||
dialog={dialog}
|
|
||||||
file={file}
|
|
||||||
comments={comments}
|
|
||||||
hasReview={hasReview()}
|
|
||||||
reviewCount={reviewCount()}
|
|
||||||
reviewTab={reviewTab()}
|
|
||||||
contextOpen={contextOpen}
|
|
||||||
openedTabs={openedTabs}
|
|
||||||
activeTab={activeTab}
|
|
||||||
activeFileTab={activeFileTab}
|
|
||||||
tabs={tabs}
|
|
||||||
openTab={openTab}
|
|
||||||
showAllFiles={showAllFiles}
|
|
||||||
reviewPanel={reviewPanel}
|
|
||||||
vm={{
|
|
||||||
messages,
|
|
||||||
visibleUserMessages,
|
|
||||||
view,
|
|
||||||
info,
|
|
||||||
}}
|
|
||||||
handoffFiles={() => handoff.session.get(sessionKey())?.files}
|
|
||||||
codeComponent={codeComponent}
|
|
||||||
addCommentToContext={addCommentToContext}
|
|
||||||
activeDraggable={() => store.activeDraggable}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
fileTreeTab={fileTreeTab}
|
|
||||||
setFileTreeTabValue={setFileTreeTabValue}
|
|
||||||
diffsReady={diffsReady()}
|
|
||||||
diffFiles={diffFiles()}
|
|
||||||
kinds={kinds()}
|
|
||||||
activeDiff={tree.activeDiff}
|
|
||||||
focusReviewDiff={focusReviewDiff}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TerminalPanel
|
<TerminalPanel />
|
||||||
open={isDesktop() && view().terminal.opened()}
|
|
||||||
height={layout.terminal.height()}
|
|
||||||
resize={layout.terminal.resize}
|
|
||||||
close={view().terminal.close}
|
|
||||||
terminal={terminal}
|
|
||||||
language={language}
|
|
||||||
command={command}
|
|
||||||
handoff={() => handoff.terminal.get(params.dir!) ?? []}
|
|
||||||
activeTerminalDraggable={() => store.activeTerminalDraggable}
|
|
||||||
handleTerminalDragStart={handleTerminalDragStart}
|
|
||||||
handleTerminalDragOver={handleTerminalDragOver}
|
|
||||||
handleTerminalDragEnd={handleTerminalDragEnd}
|
|
||||||
onCloseTab={() => setUi("autoCreated", false)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { type ValidComponent, createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js"
|
import { createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
|
import { useParams } from "@solidjs/router"
|
||||||
|
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||||
import { sampledChecksum } from "@opencode-ai/util/encode"
|
import { sampledChecksum } from "@opencode-ai/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
@@ -8,9 +10,11 @@ import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/
|
|||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useFile, type SelectedLineRange } from "@/context/file"
|
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePrompt } from "@/context/prompt"
|
||||||
|
import { getSessionHandoff } from "@/pages/session/handoff"
|
||||||
|
|
||||||
const formatCommentLabel = (range: SelectedLineRange) => {
|
const formatCommentLabel = (range: SelectedLineRange) => {
|
||||||
const start = Math.min(range.start, range.end)
|
const start = Math.min(range.start, range.end)
|
||||||
@@ -19,34 +23,29 @@ const formatCommentLabel = (range: SelectedLineRange) => {
|
|||||||
return `lines ${start}-${end}`
|
return `lines ${start}-${end}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileTabContent(props: {
|
export function FileTabContent(props: { tab: string }) {
|
||||||
tab: string
|
const params = useParams()
|
||||||
activeTab: () => string
|
const layout = useLayout()
|
||||||
tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
const file = useFile()
|
||||||
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
const comments = useComments()
|
||||||
handoffFiles: () => Record<string, SelectedLineRange | null> | undefined
|
const language = useLanguage()
|
||||||
file: ReturnType<typeof useFile>
|
const prompt = usePrompt()
|
||||||
comments: ReturnType<typeof useComments>
|
const codeComponent = useCodeComponent()
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
codeComponent: NonNullable<ValidComponent>
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
addCommentToContext: (input: {
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
file: string
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
selection: SelectedLineRange
|
|
||||||
comment: string
|
|
||||||
preview?: string
|
|
||||||
origin?: "review" | "file"
|
|
||||||
}) => void
|
|
||||||
}) {
|
|
||||||
let scroll: HTMLDivElement | undefined
|
let scroll: HTMLDivElement | undefined
|
||||||
let scrollFrame: number | undefined
|
let scrollFrame: number | undefined
|
||||||
let pending: { x: number; y: number } | undefined
|
let pending: { x: number; y: number } | undefined
|
||||||
let codeScroll: HTMLElement[] = []
|
let codeScroll: HTMLElement[] = []
|
||||||
|
|
||||||
const path = createMemo(() => props.file.pathFromTab(props.tab))
|
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||||
const state = createMemo(() => {
|
const state = createMemo(() => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return
|
if (!p) return
|
||||||
return props.file.get(p)
|
return file.get(p)
|
||||||
})
|
})
|
||||||
const contents = createMemo(() => state()?.content?.content ?? "")
|
const contents = createMemo(() => state()?.content?.content ?? "")
|
||||||
const cacheKey = createMemo(() => sampledChecksum(contents()))
|
const cacheKey = createMemo(() => sampledChecksum(contents()))
|
||||||
@@ -82,7 +81,7 @@ export function FileTabContent(props: {
|
|||||||
svgToast.shown = true
|
svgToast.shown = true
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: props.language.t("toast.file.loadFailed.title"),
|
title: language.t("toast.file.loadFailed.title"),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const svgPreviewUrl = createMemo(() => {
|
const svgPreviewUrl = createMemo(() => {
|
||||||
@@ -100,16 +99,57 @@ export function FileTabContent(props: {
|
|||||||
const selectedLines = createMemo(() => {
|
const selectedLines = createMemo(() => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return null
|
if (!p) return null
|
||||||
if (props.file.ready()) return props.file.selectedLines(p) ?? null
|
if (file.ready()) return file.selectedLines(p) ?? null
|
||||||
return props.handoffFiles()?.[p] ?? null
|
return getSessionHandoff(sessionKey())?.files[p] ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectionPreview = (source: string, selection: FileSelection) => {
|
||||||
|
const start = Math.max(1, Math.min(selection.startLine, selection.endLine))
|
||||||
|
const end = Math.max(selection.startLine, selection.endLine)
|
||||||
|
const lines = source.split("\n").slice(start - 1, end)
|
||||||
|
if (lines.length === 0) return undefined
|
||||||
|
return lines.slice(0, 2).join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCommentToContext = (input: {
|
||||||
|
file: string
|
||||||
|
selection: SelectedLineRange
|
||||||
|
comment: string
|
||||||
|
preview?: string
|
||||||
|
origin?: "review" | "file"
|
||||||
|
}) => {
|
||||||
|
const selection = selectionFromLines(input.selection)
|
||||||
|
const preview =
|
||||||
|
input.preview ??
|
||||||
|
(() => {
|
||||||
|
if (input.file === path()) return selectionPreview(contents(), selection)
|
||||||
|
const source = file.get(input.file)?.content?.content
|
||||||
|
if (!source) return undefined
|
||||||
|
return selectionPreview(source, selection)
|
||||||
|
})()
|
||||||
|
|
||||||
|
const saved = comments.add({
|
||||||
|
file: input.file,
|
||||||
|
selection: input.selection,
|
||||||
|
comment: input.comment,
|
||||||
|
})
|
||||||
|
prompt.context.add({
|
||||||
|
type: "file",
|
||||||
|
path: input.file,
|
||||||
|
selection,
|
||||||
|
comment: input.comment,
|
||||||
|
commentID: saved.id,
|
||||||
|
commentOrigin: input.origin,
|
||||||
|
preview,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let wrap: HTMLDivElement | undefined
|
let wrap: HTMLDivElement | undefined
|
||||||
|
|
||||||
const fileComments = createMemo(() => {
|
const fileComments = createMemo(() => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return []
|
if (!p) return []
|
||||||
return props.comments.list(p)
|
return comments.list(p)
|
||||||
})
|
})
|
||||||
|
|
||||||
const commentLayout = createMemo(() => {
|
const commentLayout = createMemo(() => {
|
||||||
@@ -128,6 +168,13 @@ export function FileTabContent(props: {
|
|||||||
draftTop: undefined as number | undefined,
|
draftTop: undefined as number | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const setCommenting = (range: SelectedLineRange | null) => {
|
||||||
|
setNote("commenting", range)
|
||||||
|
scheduleComments()
|
||||||
|
if (!range) return
|
||||||
|
setNote("draft", "")
|
||||||
|
}
|
||||||
|
|
||||||
const getRoot = () => {
|
const getRoot = () => {
|
||||||
const el = wrap
|
const el = wrap
|
||||||
if (!el) return
|
if (!el) return
|
||||||
@@ -221,26 +268,19 @@ export function FileTabContent(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const range = note.commenting
|
const focus = comments.focus()
|
||||||
scheduleComments()
|
|
||||||
if (!range) return
|
|
||||||
setNote("draft", "")
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const focus = props.comments.focus()
|
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!focus || !p) return
|
if (!focus || !p) return
|
||||||
if (focus.file !== p) return
|
if (focus.file !== p) return
|
||||||
if (props.activeTab() !== props.tab) return
|
if (tabs().active() !== props.tab) return
|
||||||
|
|
||||||
const target = fileComments().find((comment) => comment.id === focus.id)
|
const target = fileComments().find((comment) => comment.id === focus.id)
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
setNote("openedComment", target.id)
|
setNote("openedComment", target.id)
|
||||||
setNote("commenting", null)
|
setCommenting(null)
|
||||||
props.file.setSelectedLines(p, target.selection)
|
file.setSelectedLines(p, target.selection)
|
||||||
requestAnimationFrame(() => props.comments.clearFocus())
|
requestAnimationFrame(() => comments.clearFocus())
|
||||||
})
|
})
|
||||||
|
|
||||||
const getCodeScroll = () => {
|
const getCodeScroll = () => {
|
||||||
@@ -269,7 +309,7 @@ export function FileTabContent(props: {
|
|||||||
pending = undefined
|
pending = undefined
|
||||||
if (!out) return
|
if (!out) return
|
||||||
|
|
||||||
props.view().setScroll(props.tab, out)
|
view().setScroll(props.tab, out)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +345,7 @@ export function FileTabContent(props: {
|
|||||||
const el = scroll
|
const el = scroll
|
||||||
if (!el) return
|
if (!el) return
|
||||||
|
|
||||||
const s = props.view()?.scroll(props.tab)
|
const s = view().scroll(props.tab)
|
||||||
if (!s) return
|
if (!s) return
|
||||||
|
|
||||||
syncCodeScroll()
|
syncCodeScroll()
|
||||||
@@ -343,7 +383,7 @@ export function FileTabContent(props: {
|
|||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.file.ready(),
|
() => file.ready(),
|
||||||
(ready) => {
|
(ready) => {
|
||||||
if (!ready) return
|
if (!ready) return
|
||||||
requestAnimationFrame(restoreScroll)
|
requestAnimationFrame(restoreScroll)
|
||||||
@@ -354,7 +394,7 @@ export function FileTabContent(props: {
|
|||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.tabs().active() === props.tab,
|
() => tabs().active() === props.tab,
|
||||||
(active) => {
|
(active) => {
|
||||||
if (!active) return
|
if (!active) return
|
||||||
if (!state()?.loaded) return
|
if (!state()?.loaded) return
|
||||||
@@ -381,7 +421,7 @@ export function FileTabContent(props: {
|
|||||||
class={`relative overflow-hidden ${wrapperClass}`}
|
class={`relative overflow-hidden ${wrapperClass}`}
|
||||||
>
|
>
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={props.codeComponent}
|
component={codeComponent}
|
||||||
file={{
|
file={{
|
||||||
name: path() ?? "",
|
name: path() ?? "",
|
||||||
contents: source,
|
contents: source,
|
||||||
@@ -397,17 +437,17 @@ export function FileTabContent(props: {
|
|||||||
onLineSelected={(range: SelectedLineRange | null) => {
|
onLineSelected={(range: SelectedLineRange | null) => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return
|
if (!p) return
|
||||||
props.file.setSelectedLines(p, range)
|
file.setSelectedLines(p, range)
|
||||||
if (!range) setNote("commenting", null)
|
if (!range) setCommenting(null)
|
||||||
}}
|
}}
|
||||||
onLineSelectionEnd={(range: SelectedLineRange | null) => {
|
onLineSelectionEnd={(range: SelectedLineRange | null) => {
|
||||||
if (!range) {
|
if (!range) {
|
||||||
setNote("commenting", null)
|
setCommenting(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setNote("openedComment", null)
|
setNote("openedComment", null)
|
||||||
setNote("commenting", range)
|
setCommenting(range)
|
||||||
}}
|
}}
|
||||||
overflow="scroll"
|
overflow="scroll"
|
||||||
class="select-text"
|
class="select-text"
|
||||||
@@ -423,14 +463,14 @@ export function FileTabContent(props: {
|
|||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return
|
if (!p) return
|
||||||
props.file.setSelectedLines(p, comment.selection)
|
file.setSelectedLines(p, comment.selection)
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return
|
if (!p) return
|
||||||
setNote("commenting", null)
|
setCommenting(null)
|
||||||
setNote("openedComment", (current) => (current === comment.id ? null : comment.id))
|
setNote("openedComment", (current) => (current === comment.id ? null : comment.id))
|
||||||
props.file.setSelectedLines(p, comment.selection)
|
file.setSelectedLines(p, comment.selection)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -443,17 +483,12 @@ export function FileTabContent(props: {
|
|||||||
value={note.draft}
|
value={note.draft}
|
||||||
selection={formatCommentLabel(range())}
|
selection={formatCommentLabel(range())}
|
||||||
onInput={(value) => setNote("draft", value)}
|
onInput={(value) => setNote("draft", value)}
|
||||||
onCancel={() => setNote("commenting", null)}
|
onCancel={() => setCommenting(null)}
|
||||||
onSubmit={(value) => {
|
onSubmit={(value) => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return
|
if (!p) return
|
||||||
props.addCommentToContext({
|
addCommentToContext({ file: p, selection: range(), comment: value, origin: "file" })
|
||||||
file: p,
|
setCommenting(null)
|
||||||
selection: range(),
|
|
||||||
comment: value,
|
|
||||||
origin: "file",
|
|
||||||
})
|
|
||||||
setNote("commenting", null)
|
|
||||||
}}
|
}}
|
||||||
onPopoverFocusOut={(e: FocusEvent) => {
|
onPopoverFocusOut={(e: FocusEvent) => {
|
||||||
const current = e.currentTarget as HTMLDivElement
|
const current = e.currentTarget as HTMLDivElement
|
||||||
@@ -462,7 +497,7 @@ export function FileTabContent(props: {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!document.activeElement || !current.contains(document.activeElement)) {
|
if (!document.activeElement || !current.contains(document.activeElement)) {
|
||||||
setNote("commenting", null)
|
setCommenting(null)
|
||||||
}
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
}}
|
}}
|
||||||
@@ -509,13 +544,13 @@ export function FileTabContent(props: {
|
|||||||
<Mark class="w-14 opacity-10" />
|
<Mark class="w-14 opacity-10" />
|
||||||
<div class="flex flex-col gap-2 max-w-md">
|
<div class="flex flex-col gap-2 max-w-md">
|
||||||
<div class="text-14-semibold text-text-strong truncate">{path()?.split("/").pop()}</div>
|
<div class="text-14-semibold text-text-strong truncate">{path()?.split("/").pop()}</div>
|
||||||
<div class="text-14-regular text-text-weak">{props.language.t("session.files.binaryContent")}</div>
|
<div class="text-14-regular text-text-weak">{language.t("session.files.binaryContent")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
|
<Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
|
||||||
<Match when={state()?.loading}>
|
<Match when={state()?.loading}>
|
||||||
<div class="px-6 py-4 text-text-weak">{props.language.t("common.loading")}...</div>
|
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
|
<Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { SelectedLineRange } from "@/context/file"
|
||||||
|
|
||||||
|
type HandoffSession = {
|
||||||
|
prompt: string
|
||||||
|
files: Record<string, SelectedLineRange | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX = 40
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
session: new Map<string, HandoffSession>(),
|
||||||
|
terminal: new Map<string, string[]>(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const touch = <K, V>(map: Map<K, V>, key: K, value: V) => {
|
||||||
|
map.delete(key)
|
||||||
|
map.set(key, value)
|
||||||
|
while (map.size > MAX) {
|
||||||
|
const first = map.keys().next().value
|
||||||
|
if (first === undefined) return
|
||||||
|
map.delete(first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setSessionHandoff = (key: string, patch: Partial<HandoffSession>) => {
|
||||||
|
const prev = store.session.get(key) ?? { prompt: "", files: {} }
|
||||||
|
touch(store.session, key, { ...prev, ...patch })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSessionHandoff = (key: string) => store.session.get(key)
|
||||||
|
|
||||||
|
export const setTerminalHandoff = (key: string, value: string[]) => {
|
||||||
|
touch(store.terminal, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTerminalHandoff = (key: string) => store.terminal.get(key)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "./helpers"
|
import { createOpenReviewFile, createOpenSessionFileTab, focusTerminalById, getTabReorderIndex } from "./helpers"
|
||||||
|
|
||||||
describe("createOpenReviewFile", () => {
|
describe("createOpenReviewFile", () => {
|
||||||
test("opens and loads selected review file", () => {
|
test("opens and loads selected review file", () => {
|
||||||
@@ -20,6 +20,37 @@ describe("createOpenReviewFile", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("createOpenSessionFileTab", () => {
|
||||||
|
test("activates the opened file tab", () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const openTab = createOpenSessionFileTab({
|
||||||
|
normalizeTab: (value) => {
|
||||||
|
calls.push(`normalize:${value}`)
|
||||||
|
return `file://${value}`
|
||||||
|
},
|
||||||
|
openTab: (tab) => calls.push(`open:${tab}`),
|
||||||
|
pathFromTab: (tab) => {
|
||||||
|
calls.push(`path:${tab}`)
|
||||||
|
return tab.slice("file://".length)
|
||||||
|
},
|
||||||
|
loadFile: (path) => calls.push(`load:${path}`),
|
||||||
|
openReviewPanel: () => calls.push("review"),
|
||||||
|
setActive: (tab) => calls.push(`active:${tab}`),
|
||||||
|
})
|
||||||
|
|
||||||
|
openTab("src/a.ts")
|
||||||
|
|
||||||
|
expect(calls).toEqual([
|
||||||
|
"normalize:src/a.ts",
|
||||||
|
"open:file://src/a.ts",
|
||||||
|
"path:file://src/a.ts",
|
||||||
|
"load:src/a.ts",
|
||||||
|
"review",
|
||||||
|
"active:file://src/a.ts",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("focusTerminalById", () => {
|
describe("focusTerminalById", () => {
|
||||||
test("focuses textarea when present", () => {
|
test("focuses textarea when present", () => {
|
||||||
document.body.innerHTML = `<div id="terminal-wrapper-one"><div data-component="terminal"><textarea></textarea></div></div>`
|
document.body.innerHTML = `<div id="terminal-wrapper-one"><div data-component="terminal"><textarea></textarea></div></div>`
|
||||||
|
|||||||
@@ -35,6 +35,27 @@ export const createOpenReviewFile = (input: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createOpenSessionFileTab = (input: {
|
||||||
|
normalizeTab: (tab: string) => string
|
||||||
|
openTab: (tab: string) => void
|
||||||
|
pathFromTab: (tab: string) => string | undefined
|
||||||
|
loadFile: (path: string) => void
|
||||||
|
openReviewPanel: () => void
|
||||||
|
setActive: (tab: string) => void
|
||||||
|
}) => {
|
||||||
|
return (value: string) => {
|
||||||
|
const next = input.normalizeTab(value)
|
||||||
|
input.openTab(next)
|
||||||
|
|
||||||
|
const path = input.pathFromTab(next)
|
||||||
|
if (!path) return
|
||||||
|
|
||||||
|
input.loadFile(path)
|
||||||
|
input.openReviewPanel()
|
||||||
|
input.setActive(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => {
|
export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => {
|
||||||
const fromIndex = tabs.indexOf(from)
|
const fromIndex = tabs.indexOf(from)
|
||||||
const toIndex = tabs.indexOf(to)
|
const toIndex = tabs.indexOf(to)
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import { For, onCleanup, onMount, Show, type JSX } from "solid-js"
|
import { For, createEffect, createMemo, on, onCleanup, Show, type JSX } from "solid-js"
|
||||||
|
import { createStore, produce } from "solid-js/store"
|
||||||
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
|
||||||
const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
|
const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
|
||||||
const current = target instanceof Element ? target : undefined
|
const current = target instanceof Element ? target : undefined
|
||||||
@@ -53,29 +61,7 @@ export function MessageTimeline(props: {
|
|||||||
isDesktop: boolean
|
isDesktop: boolean
|
||||||
onScrollSpyScroll: () => void
|
onScrollSpyScroll: () => void
|
||||||
onAutoScrollInteraction: (event: MouseEvent) => void
|
onAutoScrollInteraction: (event: MouseEvent) => void
|
||||||
showHeader: boolean
|
|
||||||
centered: boolean
|
centered: boolean
|
||||||
title?: string
|
|
||||||
parentID?: string
|
|
||||||
openTitleEditor: () => void
|
|
||||||
closeTitleEditor: () => void
|
|
||||||
saveTitleEditor: () => void | Promise<void>
|
|
||||||
titleRef: (el: HTMLInputElement) => void
|
|
||||||
titleState: {
|
|
||||||
draft: string
|
|
||||||
editing: boolean
|
|
||||||
saving: boolean
|
|
||||||
menuOpen: boolean
|
|
||||||
pendingRename: boolean
|
|
||||||
}
|
|
||||||
onTitleDraft: (value: string) => void
|
|
||||||
onTitleMenuOpen: (open: boolean) => void
|
|
||||||
onTitlePendingRename: (value: boolean) => void
|
|
||||||
onNavigateParent: () => void
|
|
||||||
sessionID: string
|
|
||||||
onArchiveSession: (sessionID: string) => void
|
|
||||||
onDeleteSession: (sessionID: string) => void
|
|
||||||
t: (key: string, vars?: Record<string, string | number | boolean>) => string
|
|
||||||
setContentRef: (el: HTMLDivElement) => void
|
setContentRef: (el: HTMLDivElement) => void
|
||||||
turnStart: number
|
turnStart: number
|
||||||
onRenderEarlier: () => void
|
onRenderEarlier: () => void
|
||||||
@@ -86,11 +72,234 @@ export function MessageTimeline(props: {
|
|||||||
anchor: (id: string) => string
|
anchor: (id: string) => string
|
||||||
onRegisterMessage: (el: HTMLDivElement, id: string) => void
|
onRegisterMessage: (el: HTMLDivElement, id: string) => void
|
||||||
onUnregisterMessage: (id: string) => void
|
onUnregisterMessage: (id: string) => void
|
||||||
onFirstTurnMount?: () => void
|
|
||||||
lastUserMessageID?: string
|
lastUserMessageID?: string
|
||||||
}) {
|
}) {
|
||||||
let touchGesture: number | undefined
|
let touchGesture: number | undefined
|
||||||
|
|
||||||
|
const params = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const sync = useSync()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
|
const sessionID = createMemo(() => params.id)
|
||||||
|
const info = createMemo(() => {
|
||||||
|
const id = sessionID()
|
||||||
|
if (!id) return
|
||||||
|
return sync.session.get(id)
|
||||||
|
})
|
||||||
|
const titleValue = createMemo(() => info()?.title)
|
||||||
|
const parentID = createMemo(() => info()?.parentID)
|
||||||
|
const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
||||||
|
|
||||||
|
const [title, setTitle] = createStore({
|
||||||
|
draft: "",
|
||||||
|
editing: false,
|
||||||
|
saving: false,
|
||||||
|
menuOpen: false,
|
||||||
|
pendingRename: false,
|
||||||
|
})
|
||||||
|
let titleRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
|
const errorMessage = (err: unknown) => {
|
||||||
|
if (err && typeof err === "object" && "data" in err) {
|
||||||
|
const data = (err as { data?: { message?: string } }).data
|
||||||
|
if (data?.message) return data.message
|
||||||
|
}
|
||||||
|
if (err instanceof Error) return err.message
|
||||||
|
return language.t("common.requestFailed")
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
sessionKey,
|
||||||
|
() => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const openTitleEditor = () => {
|
||||||
|
if (!sessionID()) return
|
||||||
|
setTitle({ editing: true, draft: titleValue() ?? "" })
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
titleRef?.focus()
|
||||||
|
titleRef?.select()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeTitleEditor = () => {
|
||||||
|
if (title.saving) return
|
||||||
|
setTitle({ editing: false, saving: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveTitleEditor = async () => {
|
||||||
|
const id = sessionID()
|
||||||
|
if (!id) return
|
||||||
|
if (title.saving) return
|
||||||
|
|
||||||
|
const next = title.draft.trim()
|
||||||
|
if (!next || next === (titleValue() ?? "")) {
|
||||||
|
setTitle({ editing: false, saving: false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle("saving", true)
|
||||||
|
await sdk.client.session
|
||||||
|
.update({ sessionID: id, title: next })
|
||||||
|
.then(() => {
|
||||||
|
sync.set(
|
||||||
|
produce((draft) => {
|
||||||
|
const index = draft.session.findIndex((s) => s.id === id)
|
||||||
|
if (index !== -1) draft.session[index].title = next
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
setTitle({ editing: false, saving: false })
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setTitle("saving", false)
|
||||||
|
showToast({
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: errorMessage(err),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
||||||
|
if (params.id !== sessionID) return
|
||||||
|
if (parentID) {
|
||||||
|
navigate(`/${params.dir}/session/${parentID}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (nextSessionID) {
|
||||||
|
navigate(`/${params.dir}/session/${nextSessionID}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigate(`/${params.dir}/session`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const archiveSession = async (sessionID: string) => {
|
||||||
|
const session = sync.session.get(sessionID)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
const sessions = sync.data.session ?? []
|
||||||
|
const index = sessions.findIndex((s) => s.id === sessionID)
|
||||||
|
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
||||||
|
|
||||||
|
await sdk.client.session
|
||||||
|
.update({ sessionID, time: { archived: Date.now() } })
|
||||||
|
.then(() => {
|
||||||
|
sync.set(
|
||||||
|
produce((draft) => {
|
||||||
|
const index = draft.session.findIndex((s) => s.id === sessionID)
|
||||||
|
if (index !== -1) draft.session.splice(index, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
showToast({
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: errorMessage(err),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSession = async (sessionID: string) => {
|
||||||
|
const session = sync.session.get(sessionID)
|
||||||
|
if (!session) return false
|
||||||
|
|
||||||
|
const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
|
||||||
|
const index = sessions.findIndex((s) => s.id === sessionID)
|
||||||
|
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
||||||
|
|
||||||
|
const result = await sdk.client.session
|
||||||
|
.delete({ sessionID })
|
||||||
|
.then((x) => x.data)
|
||||||
|
.catch((err) => {
|
||||||
|
showToast({
|
||||||
|
title: language.t("session.delete.failed.title"),
|
||||||
|
description: errorMessage(err),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) return false
|
||||||
|
|
||||||
|
sync.set(
|
||||||
|
produce((draft) => {
|
||||||
|
const removed = new Set<string>([sessionID])
|
||||||
|
|
||||||
|
const byParent = new Map<string, string[]>()
|
||||||
|
for (const item of draft.session) {
|
||||||
|
const parentID = item.parentID
|
||||||
|
if (!parentID) continue
|
||||||
|
const existing = byParent.get(parentID)
|
||||||
|
if (existing) {
|
||||||
|
existing.push(item.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byParent.set(parentID, [item.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = [sessionID]
|
||||||
|
while (stack.length) {
|
||||||
|
const parentID = stack.pop()
|
||||||
|
if (!parentID) continue
|
||||||
|
|
||||||
|
const children = byParent.get(parentID)
|
||||||
|
if (!children) continue
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
if (removed.has(child)) continue
|
||||||
|
removed.add(child)
|
||||||
|
stack.push(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateParent = () => {
|
||||||
|
const id = parentID()
|
||||||
|
if (!id) return
|
||||||
|
navigate(`/${params.dir}/session/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDeleteSession(props: { sessionID: string }) {
|
||||||
|
const name = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new"))
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await deleteSession(props.sessionID)
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog title={language.t("session.delete.title")} fit>
|
||||||
|
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-14-regular text-text-strong">
|
||||||
|
{language.t("session.delete.confirm", { name: name() })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||||
|
{language.t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" size="large" onClick={handleDelete}>
|
||||||
|
{language.t("session.delete.button")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={!props.mobileChanges}
|
when={!props.mobileChanges}
|
||||||
@@ -157,106 +366,110 @@ export function MessageTimeline(props: {
|
|||||||
}}
|
}}
|
||||||
onClick={props.onAutoScrollInteraction}
|
onClick={props.onAutoScrollInteraction}
|
||||||
class="relative min-w-0 w-full h-full overflow-y-auto session-scroller"
|
class="relative min-w-0 w-full h-full overflow-y-auto session-scroller"
|
||||||
style={{ "--session-title-height": props.showHeader ? "40px" : "0px" }}
|
style={{ "--session-title-height": showHeader() ? "40px" : "0px" }}
|
||||||
>
|
>
|
||||||
<Show when={props.showHeader}>
|
<Show when={showHeader()}>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
||||||
"w-full": true,
|
"w-full": true,
|
||||||
"pb-4": true,
|
"pb-4": true,
|
||||||
"pl-2 pr-4 md:pl-4 md:pr-6": true,
|
"pl-2 pr-3 md:pl-4 md:pr-3": true,
|
||||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="h-12 w-full flex items-center justify-between gap-2">
|
<div class="h-12 w-full flex items-center justify-between gap-2">
|
||||||
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
||||||
<Show when={props.parentID}>
|
<Show when={parentID()}>
|
||||||
<IconButton
|
<IconButton
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
icon="arrow-left"
|
icon="arrow-left"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={props.onNavigateParent}
|
onClick={navigateParent}
|
||||||
aria-label={props.t("common.goBack")}
|
aria-label={language.t("common.goBack")}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.title || props.titleState.editing}>
|
<Show when={titleValue() || title.editing}>
|
||||||
<Show
|
<Show
|
||||||
when={props.titleState.editing}
|
when={title.editing}
|
||||||
fallback={
|
fallback={
|
||||||
<h1
|
<h1
|
||||||
class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
|
class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
|
||||||
onDblClick={props.openTitleEditor}
|
onDblClick={openTitleEditor}
|
||||||
>
|
>
|
||||||
{props.title}
|
{titleValue()}
|
||||||
</h1>
|
</h1>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<InlineInput
|
<InlineInput
|
||||||
ref={props.titleRef}
|
ref={(el) => {
|
||||||
value={props.titleState.draft}
|
titleRef = el
|
||||||
disabled={props.titleState.saving}
|
}}
|
||||||
|
value={title.draft}
|
||||||
|
disabled={title.saving}
|
||||||
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
|
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
|
||||||
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||||
onInput={(event) => props.onTitleDraft(event.currentTarget.value)}
|
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
void props.saveTitleEditor()
|
void saveTitleEditor()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
props.closeTitleEditor()
|
closeTitleEditor()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={props.closeTitleEditor}
|
onBlur={closeTitleEditor}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.sessionID}>
|
<Show when={sessionID()}>
|
||||||
{(id) => (
|
{(id) => (
|
||||||
<div class="shrink-0 flex items-center gap-3">
|
<div class="shrink-0 flex items-center gap-3">
|
||||||
<SessionContextUsage placement="bottom" />
|
<SessionContextUsage placement="bottom" />
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
gutter={4}
|
gutter={4}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
open={props.titleState.menuOpen}
|
open={title.menuOpen}
|
||||||
onOpenChange={props.onTitleMenuOpen}
|
onOpenChange={(open) => setTitle("menuOpen", open)}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
icon="dot-grid"
|
icon="dot-grid"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||||
aria-label={props.t("common.moreOptions")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
style={{ "min-width": "104px" }}
|
style={{ "min-width": "104px" }}
|
||||||
onCloseAutoFocus={(event) => {
|
onCloseAutoFocus={(event) => {
|
||||||
if (!props.titleState.pendingRename) return
|
if (!title.pendingRename) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
props.onTitlePendingRename(false)
|
setTitle("pendingRename", false)
|
||||||
props.openTitleEditor()
|
openTitleEditor()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
props.onTitlePendingRename(true)
|
setTitle("pendingRename", true)
|
||||||
props.onTitleMenuOpen(false)
|
setTitle("menuOpen", false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{props.t("common.rename")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onSelect={() => props.onArchiveSession(id())}>
|
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||||
<DropdownMenu.ItemLabel>{props.t("common.archive")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item onSelect={() => props.onDeleteSession(id())}>
|
<DropdownMenu.Item
|
||||||
<DropdownMenu.ItemLabel>{props.t("common.delete")}</DropdownMenu.ItemLabel>
|
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
@@ -282,7 +495,7 @@ export function MessageTimeline(props: {
|
|||||||
<Show when={props.turnStart > 0}>
|
<Show when={props.turnStart > 0}>
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<Button variant="ghost" size="large" class="text-12-medium opacity-50" onClick={props.onRenderEarlier}>
|
<Button variant="ghost" size="large" class="text-12-medium opacity-50" onClick={props.onRenderEarlier}>
|
||||||
{props.t("session.messages.renderEarlier")}
|
{language.t("session.messages.renderEarlier")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -296,43 +509,37 @@ export function MessageTimeline(props: {
|
|||||||
onClick={props.onLoadEarlier}
|
onClick={props.onLoadEarlier}
|
||||||
>
|
>
|
||||||
{props.historyLoading
|
{props.historyLoading
|
||||||
? props.t("session.messages.loadingEarlier")
|
? language.t("session.messages.loadingEarlier")
|
||||||
: props.t("session.messages.loadEarlier")}
|
: language.t("session.messages.loadEarlier")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<For each={props.renderedUserMessages}>
|
<For each={props.renderedUserMessages}>
|
||||||
{(message) => {
|
{(message) => (
|
||||||
if (import.meta.env.DEV && props.onFirstTurnMount) {
|
<div
|
||||||
onMount(() => props.onFirstTurnMount?.())
|
id={props.anchor(message.id)}
|
||||||
}
|
data-message-id={message.id}
|
||||||
|
ref={(el) => {
|
||||||
return (
|
props.onRegisterMessage(el, message.id)
|
||||||
<div
|
onCleanup(() => props.onUnregisterMessage(message.id))
|
||||||
id={props.anchor(message.id)}
|
}}
|
||||||
data-message-id={message.id}
|
classList={{
|
||||||
ref={(el) => {
|
"min-w-0 w-full max-w-full": true,
|
||||||
props.onRegisterMessage(el, message.id)
|
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||||
onCleanup(() => props.onUnregisterMessage(message.id))
|
}}
|
||||||
|
>
|
||||||
|
<SessionTurn
|
||||||
|
sessionID={sessionID() ?? ""}
|
||||||
|
messageID={message.id}
|
||||||
|
lastUserMessageID={props.lastUserMessageID}
|
||||||
|
classes={{
|
||||||
|
root: "min-w-0 w-full relative",
|
||||||
|
content: "flex flex-col justify-between !overflow-visible",
|
||||||
|
container: "w-full px-4 md:px-5",
|
||||||
}}
|
}}
|
||||||
classList={{
|
/>
|
||||||
"min-w-0 w-full max-w-full": true,
|
</div>
|
||||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
)}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SessionTurn
|
|
||||||
sessionID={props.sessionID}
|
|
||||||
messageID={message.id}
|
|
||||||
lastUserMessageID={props.lastUserMessageID}
|
|
||||||
classes={{
|
|
||||||
root: "min-w-0 w-full relative",
|
|
||||||
content: "flex flex-col justify-between !overflow-visible",
|
|
||||||
container: "w-full px-4 md:px-6",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
|||||||
onOpenChange={props.view().review.setOpen}
|
onOpenChange={props.view().review.setOpen}
|
||||||
classes={{
|
classes={{
|
||||||
root: props.classes?.root ?? "pb-6",
|
root: props.classes?.root ?? "pb-6",
|
||||||
header: props.classes?.header ?? "px-6",
|
header: props.classes?.header ?? "px-3",
|
||||||
container: props.classes?.container ?? "px-6",
|
container: props.classes?.container ?? "px-3",
|
||||||
}}
|
}}
|
||||||
diffs={props.diffs()}
|
diffs={props.diffs()}
|
||||||
diffStyle={props.diffStyle}
|
diffStyle={props.diffStyle}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
export function SessionMobileTabs(props: {
|
export function SessionMobileTabs(props: {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -8,8 +9,9 @@ export function SessionMobileTabs(props: {
|
|||||||
reviewCount: number
|
reviewCount: number
|
||||||
onSession: () => void
|
onSession: () => void
|
||||||
onChanges: () => void
|
onChanges: () => void
|
||||||
t: (key: string, vars?: Record<string, string | number | boolean>) => string
|
|
||||||
}) {
|
}) {
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.open}>
|
<Show when={props.open}>
|
||||||
<Tabs value={props.mobileTab} class="h-auto">
|
<Tabs value={props.mobileTab} class="h-auto">
|
||||||
@@ -20,7 +22,7 @@ export function SessionMobileTabs(props: {
|
|||||||
classes={{ button: "w-full" }}
|
classes={{ button: "w-full" }}
|
||||||
onClick={props.onSession}
|
onClick={props.onSession}
|
||||||
>
|
>
|
||||||
{props.t("session.tab.session")}
|
{language.t("session.tab.session")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="changes"
|
value="changes"
|
||||||
@@ -29,8 +31,8 @@ export function SessionMobileTabs(props: {
|
|||||||
onClick={props.onChanges}
|
onClick={props.onChanges}
|
||||||
>
|
>
|
||||||
{props.hasReview
|
{props.hasReview
|
||||||
? props.t("session.review.filesChanged", { count: props.reviewCount })
|
? language.t("session.review.filesChanged", { count: props.reviewCount })
|
||||||
: props.t("session.review.change.other")}
|
: language.t("session.review.change.other")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,34 +1,104 @@
|
|||||||
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
||||||
import type { QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
||||||
|
import { useParams } from "@solidjs/router"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { BasicTool } from "@opencode-ai/ui/basic-tool"
|
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { PromptInput } from "@/components/prompt-input"
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
import { QuestionDock } from "@/components/question-dock"
|
import { QuestionDock } from "@/components/question-dock"
|
||||||
import { SessionTodoDock } from "@/components/session-todo-dock"
|
import { SessionTodoDock } from "@/components/session-todo-dock"
|
||||||
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePrompt } from "@/context/prompt"
|
||||||
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
||||||
|
|
||||||
export function SessionPromptDock(props: {
|
export function SessionPromptDock(props: {
|
||||||
centered: boolean
|
centered: boolean
|
||||||
questionRequest: () => QuestionRequest | undefined
|
|
||||||
permissionRequest: () => { patterns: string[]; permission: string } | undefined
|
|
||||||
blocked: boolean
|
|
||||||
todos: Todo[]
|
|
||||||
promptReady: boolean
|
|
||||||
handoffPrompt?: string
|
|
||||||
t: (key: string, vars?: Record<string, string | number | boolean>) => string
|
|
||||||
responding: boolean
|
|
||||||
onDecide: (response: "once" | "always" | "reject") => void
|
|
||||||
inputRef: (el: HTMLDivElement) => void
|
inputRef: (el: HTMLDivElement) => void
|
||||||
newSessionWorktree: string
|
newSessionWorktree: string
|
||||||
onNewSessionWorktreeReset: () => void
|
onNewSessionWorktreeReset: () => void
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
setPromptDockRef: (el: HTMLDivElement) => void
|
setPromptDockRef: (el: HTMLDivElement) => void
|
||||||
}) {
|
}) {
|
||||||
|
const params = useParams()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const sync = useSync()
|
||||||
|
const globalSync = useGlobalSync()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
|
const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt)
|
||||||
|
|
||||||
|
const todos = createMemo((): Todo[] => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return []
|
||||||
|
return globalSync.data.session_todo[id] ?? []
|
||||||
|
})
|
||||||
|
|
||||||
|
const questionRequest = createMemo((): QuestionRequest | undefined => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
return sync.data.question[sessionID]?.[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
const permissionRequest = createMemo((): PermissionRequest | undefined => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
return sync.data.permission[sessionID]?.[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
const blocked = createMemo(() => !!permissionRequest() || !!questionRequest())
|
||||||
|
|
||||||
|
const previewPrompt = () =>
|
||||||
|
prompt
|
||||||
|
.current()
|
||||||
|
.map((part) => {
|
||||||
|
if (part.type === "file") return `[file:${part.path}]`
|
||||||
|
if (part.type === "agent") return `@${part.name}`
|
||||||
|
if (part.type === "image") return `[image:${part.filename}]`
|
||||||
|
return part.content
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!prompt.ready()) return
|
||||||
|
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
||||||
|
})
|
||||||
|
|
||||||
|
const [responding, setResponding] = createSignal<string | undefined>()
|
||||||
|
const permissionResponding = () => {
|
||||||
|
const perm = permissionRequest()
|
||||||
|
if (!perm) return false
|
||||||
|
return responding() === perm.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const decide = (response: "once" | "always" | "reject") => {
|
||||||
|
const perm = permissionRequest()
|
||||||
|
if (!perm) return
|
||||||
|
if (responding() === perm.id) return
|
||||||
|
|
||||||
|
setResponding(perm.id)
|
||||||
|
sdk.client.permission
|
||||||
|
.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setResponding((id) => (id === perm.id ? undefined : id))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const done = createMemo(
|
const done = createMemo(
|
||||||
() =>
|
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
|
||||||
props.todos.length > 0 && props.todos.every((todo) => todo.status === "completed" || todo.status === "cancelled"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const [dock, setDock] = createSignal(props.todos.length > 0)
|
const [dock, setDock] = createSignal(todos().length > 0)
|
||||||
const [closing, setClosing] = createSignal(false)
|
const [closing, setClosing] = createSignal(false)
|
||||||
const [opening, setOpening] = createSignal(false)
|
const [opening, setOpening] = createSignal(false)
|
||||||
let timer: number | undefined
|
let timer: number | undefined
|
||||||
@@ -45,7 +115,7 @@ export function SessionPromptDock(props: {
|
|||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => [props.todos.length, done()] as const,
|
() => [todos().length, done()] as const,
|
||||||
([count, complete], prev) => {
|
([count, complete], prev) => {
|
||||||
if (raf) cancelAnimationFrame(raf)
|
if (raf) cancelAnimationFrame(raf)
|
||||||
raf = undefined
|
raf = undefined
|
||||||
@@ -104,15 +174,15 @@ export function SessionPromptDock(props: {
|
|||||||
<div
|
<div
|
||||||
ref={props.setPromptDockRef}
|
ref={props.setPromptDockRef}
|
||||||
data-component="session-prompt-dock"
|
data-component="session-prompt-dock"
|
||||||
class="shrink-0 w-full pb-4 flex flex-col justify-center items-center bg-background-stronger pointer-events-none"
|
class="shrink-0 w-full pb-3 flex flex-col justify-center items-center bg-background-stronger pointer-events-none"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"w-full px-4 pointer-events-auto": true,
|
"w-full px-3 pointer-events-auto": true,
|
||||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={props.questionRequest()} keyed>
|
<Show when={questionRequest()} keyed>
|
||||||
{(req) => {
|
{(req) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -122,72 +192,88 @@ export function SessionPromptDock(props: {
|
|||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.permissionRequest()} keyed>
|
<Show when={permissionRequest()} keyed>
|
||||||
{(perm) => (
|
{(perm) => {
|
||||||
<div data-component="tool-part-wrapper" data-permission="true" class="mb-3">
|
const toolDescription = () => {
|
||||||
<BasicTool
|
const key = `settings.permissions.tool.${perm.permission}.description`
|
||||||
icon="checklist"
|
const value = language.t(key as Parameters<typeof language.t>[0])
|
||||||
locked
|
if (value === key) return ""
|
||||||
defaultOpen
|
return value
|
||||||
trigger={{
|
}
|
||||||
title: props.t("notification.permission.title"),
|
|
||||||
subtitle:
|
return (
|
||||||
perm.permission === "doom_loop"
|
<div>
|
||||||
? props.t("settings.permissions.tool.doom_loop.title")
|
<DockPrompt
|
||||||
: perm.permission,
|
kind="permission"
|
||||||
}}
|
header={
|
||||||
>
|
<div data-slot="permission-row" data-variant="header">
|
||||||
<Show when={perm.patterns.length > 0}>
|
<span data-slot="permission-icon">
|
||||||
<div class="flex flex-col gap-1 py-2 px-3 max-h-40 overflow-y-auto no-scrollbar">
|
<Icon name="warning" size="normal" />
|
||||||
<For each={perm.patterns}>
|
</span>
|
||||||
{(pattern) => <code class="text-12-regular text-text-base break-all">{pattern}</code>}
|
<div data-slot="permission-header-title">{language.t("notification.permission.title")}</div>
|
||||||
</For>
|
</div>
|
||||||
</div>
|
}
|
||||||
</Show>
|
footer={
|
||||||
<Show when={perm.permission === "doom_loop"}>
|
<>
|
||||||
<div class="text-12-regular text-text-weak pb-2 px-3">
|
<div />
|
||||||
{props.t("settings.permissions.tool.doom_loop.description")}
|
<div data-slot="permission-footer-actions">
|
||||||
</div>
|
<Button
|
||||||
</Show>
|
variant="ghost"
|
||||||
</BasicTool>
|
size="normal"
|
||||||
<div data-component="permission-prompt">
|
onClick={() => decide("reject")}
|
||||||
<div data-slot="permission-actions">
|
disabled={permissionResponding()}
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
{language.t("ui.permission.deny")}
|
||||||
size="small"
|
</Button>
|
||||||
onClick={() => props.onDecide("reject")}
|
<Button
|
||||||
disabled={props.responding}
|
variant="secondary"
|
||||||
>
|
size="normal"
|
||||||
{props.t("ui.permission.deny")}
|
onClick={() => decide("always")}
|
||||||
</Button>
|
disabled={permissionResponding()}
|
||||||
<Button
|
>
|
||||||
variant="secondary"
|
{language.t("ui.permission.allowAlways")}
|
||||||
size="small"
|
</Button>
|
||||||
onClick={() => props.onDecide("always")}
|
<Button
|
||||||
disabled={props.responding}
|
variant="primary"
|
||||||
>
|
size="normal"
|
||||||
{props.t("ui.permission.allowAlways")}
|
onClick={() => decide("once")}
|
||||||
</Button>
|
disabled={permissionResponding()}
|
||||||
<Button
|
>
|
||||||
variant="primary"
|
{language.t("ui.permission.allowOnce")}
|
||||||
size="small"
|
</Button>
|
||||||
onClick={() => props.onDecide("once")}
|
</div>
|
||||||
disabled={props.responding}
|
</>
|
||||||
>
|
}
|
||||||
{props.t("ui.permission.allowOnce")}
|
>
|
||||||
</Button>
|
<Show when={toolDescription()}>
|
||||||
</div>
|
<div data-slot="permission-row">
|
||||||
|
<span data-slot="permission-spacer" aria-hidden="true" />
|
||||||
|
<div data-slot="permission-hint">{toolDescription()}</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={perm.patterns.length > 0}>
|
||||||
|
<div data-slot="permission-row">
|
||||||
|
<span data-slot="permission-spacer" aria-hidden="true" />
|
||||||
|
<div data-slot="permission-patterns">
|
||||||
|
<For each={perm.patterns}>
|
||||||
|
{(pattern) => <code class="text-12-regular text-text-base break-all">{pattern}</code>}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</DockPrompt>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.blocked}>
|
<Show when={!blocked()}>
|
||||||
<Show
|
<Show
|
||||||
when={props.promptReady}
|
when={prompt.ready()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
|
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
|
||||||
{props.handoffPrompt || props.t("prompt.loading")}
|
{handoffPrompt() || language.t("prompt.loading")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -202,10 +288,10 @@ export function SessionPromptDock(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SessionTodoDock
|
<SessionTodoDock
|
||||||
todos={props.todos}
|
todos={todos()}
|
||||||
title={props.t("session.todo.title")}
|
title={language.t("session.todo.title")}
|
||||||
collapseLabel={props.t("session.todo.collapse")}
|
collapseLabel={language.t("session.todo.collapse")}
|
||||||
expandLabel={props.t("session.todo.expand")}
|
expandLabel={language.t("session.todo.expand")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,156 +1,268 @@
|
|||||||
import { For, Match, Show, Switch, createMemo, onCleanup, type JSX, type ValidComponent } from "solid-js"
|
import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
|
import { useParams } from "@solidjs/router"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
|
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
||||||
|
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||||
|
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
|
||||||
import FileTree from "@/components/file-tree"
|
import FileTree from "@/components/file-tree"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
|
|
||||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
|
||||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
|
||||||
import { StickyAddButton } from "@/pages/session/review-tab"
|
|
||||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
|
||||||
import { ConstrainDragYAxis } from "@/utils/solid-dnd"
|
|
||||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
|
||||||
import { useComments } from "@/context/comments"
|
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { useFile, type SelectedLineRange } from "@/context/file"
|
import { useFile, type SelectedLineRange } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import type { Message, UserMessage } from "@opencode-ai/sdk/v2/client"
|
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||||
|
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||||
type SessionSidePanelViewModel = {
|
import { createOpenSessionFileTab, getTabReorderIndex } from "@/pages/session/helpers"
|
||||||
messages: () => Message[]
|
import { StickyAddButton } from "@/pages/session/review-tab"
|
||||||
visibleUserMessages: () => UserMessage[]
|
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||||
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
|
||||||
info: () => ReturnType<ReturnType<typeof useSync>["session"]["get"]>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SessionSidePanel(props: {
|
export function SessionSidePanel(props: {
|
||||||
open: boolean
|
|
||||||
reviewOpen: boolean
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
layout: ReturnType<typeof useLayout>
|
|
||||||
command: ReturnType<typeof useCommand>
|
|
||||||
dialog: ReturnType<typeof useDialog>
|
|
||||||
file: ReturnType<typeof useFile>
|
|
||||||
comments: ReturnType<typeof useComments>
|
|
||||||
hasReview: boolean
|
|
||||||
reviewCount: number
|
|
||||||
reviewTab: boolean
|
|
||||||
contextOpen: () => boolean
|
|
||||||
openedTabs: () => string[]
|
|
||||||
activeTab: () => string
|
|
||||||
activeFileTab: () => string | undefined
|
|
||||||
tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
|
||||||
openTab: (value: string) => void
|
|
||||||
showAllFiles: () => void
|
|
||||||
reviewPanel: () => JSX.Element
|
reviewPanel: () => JSX.Element
|
||||||
vm: SessionSidePanelViewModel
|
|
||||||
handoffFiles: () => Record<string, SelectedLineRange | null> | undefined
|
|
||||||
codeComponent: NonNullable<ValidComponent>
|
|
||||||
addCommentToContext: (input: {
|
|
||||||
file: string
|
|
||||||
selection: SelectedLineRange
|
|
||||||
comment: string
|
|
||||||
preview?: string
|
|
||||||
origin?: "review" | "file"
|
|
||||||
}) => void
|
|
||||||
activeDraggable: () => string | undefined
|
|
||||||
onDragStart: (event: unknown) => void
|
|
||||||
onDragEnd: () => void
|
|
||||||
onDragOver: (event: DragEvent) => void
|
|
||||||
fileTreeTab: () => "changes" | "all"
|
|
||||||
setFileTreeTabValue: (value: string) => void
|
|
||||||
diffsReady: boolean
|
|
||||||
diffFiles: string[]
|
|
||||||
kinds: Map<string, "add" | "del" | "mix">
|
|
||||||
activeDiff?: string
|
activeDiff?: string
|
||||||
focusReviewDiff: (path: string) => void
|
focusReviewDiff: (path: string) => void
|
||||||
}) {
|
}) {
|
||||||
const openedTabs = createMemo(() => props.openedTabs())
|
const params = useParams()
|
||||||
|
const layout = useLayout()
|
||||||
|
const sync = useSync()
|
||||||
|
const file = useFile()
|
||||||
|
const language = useLanguage()
|
||||||
|
const command = useCommand()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
|
||||||
|
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||||
|
const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened()))
|
||||||
|
const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened())
|
||||||
|
|
||||||
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
|
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||||
|
const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||||
|
const hasReview = createMemo(() => reviewCount() > 0)
|
||||||
|
const diffsReady = createMemo(() => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return true
|
||||||
|
if (!hasReview()) return true
|
||||||
|
return sync.data.session_diff[id] !== undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const diffFiles = createMemo(() => diffs().map((d) => d.file))
|
||||||
|
const kinds = createMemo(() => {
|
||||||
|
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
||||||
|
if (!a) return b
|
||||||
|
if (a === b) return a
|
||||||
|
return "mix" as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
||||||
|
|
||||||
|
const out = new Map<string, "add" | "del" | "mix">()
|
||||||
|
for (const diff of diffs()) {
|
||||||
|
const file = normalize(diff.file)
|
||||||
|
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
||||||
|
|
||||||
|
out.set(file, kind)
|
||||||
|
|
||||||
|
const parts = file.split("/")
|
||||||
|
for (const [idx] of parts.slice(0, -1).entries()) {
|
||||||
|
const dir = parts.slice(0, idx + 1).join("/")
|
||||||
|
if (!dir) continue
|
||||||
|
out.set(dir, merge(out.get(dir), kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeTab = (tab: string) => {
|
||||||
|
if (!tab.startsWith("file://")) return tab
|
||||||
|
return file.tab(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openReviewPanel = () => {
|
||||||
|
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openTab = createOpenSessionFileTab({
|
||||||
|
normalizeTab,
|
||||||
|
openTab: tabs().open,
|
||||||
|
pathFromTab: file.pathFromTab,
|
||||||
|
loadFile: file.load,
|
||||||
|
openReviewPanel,
|
||||||
|
setActive: tabs().setActive,
|
||||||
|
})
|
||||||
|
|
||||||
|
const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
|
||||||
|
const openedTabs = createMemo(() =>
|
||||||
|
tabs()
|
||||||
|
.all()
|
||||||
|
.filter((tab) => tab !== "context" && tab !== "review"),
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeTab = createMemo(() => {
|
||||||
|
const active = tabs().active()
|
||||||
|
if (active === "context") return "context"
|
||||||
|
if (active === "review" && reviewTab()) return "review"
|
||||||
|
if (active && file.pathFromTab(active)) return normalizeTab(active)
|
||||||
|
|
||||||
|
const first = openedTabs()[0]
|
||||||
|
if (first) return first
|
||||||
|
if (contextOpen()) return "context"
|
||||||
|
if (reviewTab() && hasReview()) return "review"
|
||||||
|
return "empty"
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeFileTab = createMemo(() => {
|
||||||
|
const active = activeTab()
|
||||||
|
if (!openedTabs().includes(active)) return
|
||||||
|
return active
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileTreeTab = () => layout.fileTree.tab()
|
||||||
|
|
||||||
|
const setFileTreeTabValue = (value: string) => {
|
||||||
|
if (value !== "changes" && value !== "all") return
|
||||||
|
layout.fileTree.setTab(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showAllFiles = () => {
|
||||||
|
if (fileTreeTab() !== "changes") return
|
||||||
|
layout.fileTree.setTab("all")
|
||||||
|
}
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
activeDraggable: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleDragStart = (event: unknown) => {
|
||||||
|
const id = getDraggableId(event)
|
||||||
|
if (!id) return
|
||||||
|
setStore("activeDraggable", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragEvent) => {
|
||||||
|
const { draggable, droppable } = event
|
||||||
|
if (!draggable || !droppable) return
|
||||||
|
|
||||||
|
const currentTabs = tabs().all()
|
||||||
|
const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString())
|
||||||
|
if (toIndex === undefined) return
|
||||||
|
tabs().move(draggable.id.toString(), toIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
setStore("activeDraggable", undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!file.ready()) return
|
||||||
|
|
||||||
|
setSessionHandoff(sessionKey(), {
|
||||||
|
files: tabs()
|
||||||
|
.all()
|
||||||
|
.reduce<Record<string, SelectedLineRange | null>>((acc, tab) => {
|
||||||
|
const path = file.pathFromTab(tab)
|
||||||
|
if (!path) return acc
|
||||||
|
|
||||||
|
const selected = file.selectedLines(path)
|
||||||
|
acc[path] =
|
||||||
|
selected && typeof selected === "object" && "start" in selected && "end" in selected
|
||||||
|
? (selected as SelectedLineRange)
|
||||||
|
: null
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.open}>
|
<Show when={open()}>
|
||||||
<aside
|
<aside
|
||||||
id="review-panel"
|
id="review-panel"
|
||||||
aria-label={props.language.t("session.panel.reviewAndFiles")}
|
aria-label={language.t("session.panel.reviewAndFiles")}
|
||||||
class="relative min-w-0 h-full border-l border-border-weak-base flex"
|
class="relative min-w-0 h-full border-l border-border-weak-base flex"
|
||||||
classList={{
|
classList={{
|
||||||
"flex-1": props.reviewOpen,
|
"flex-1": reviewOpen(),
|
||||||
"shrink-0": !props.reviewOpen,
|
"shrink-0": !reviewOpen(),
|
||||||
}}
|
}}
|
||||||
style={{ width: props.reviewOpen ? undefined : `${props.layout.fileTree.width()}px` }}
|
style={{ width: reviewOpen() ? undefined : `${layout.fileTree.width()}px` }}
|
||||||
>
|
>
|
||||||
<Show when={props.reviewOpen}>
|
<Show when={reviewOpen()}>
|
||||||
<div class="flex-1 min-w-0 h-full">
|
<div class="flex-1 min-w-0 h-full">
|
||||||
<Show
|
<Show
|
||||||
when={props.layout.fileTree.opened() && props.fileTreeTab() === "changes"}
|
when={layout.fileTree.opened() && fileTreeTab() === "changes"}
|
||||||
fallback={
|
fallback={
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
onDragStart={props.onDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={props.onDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
onDragOver={props.onDragOver}
|
onDragOver={handleDragOver}
|
||||||
collisionDetector={closestCenter}
|
collisionDetector={closestCenter}
|
||||||
>
|
>
|
||||||
<DragDropSensors />
|
<DragDropSensors />
|
||||||
<ConstrainDragYAxis />
|
<ConstrainDragYAxis />
|
||||||
<Tabs value={props.activeTab()} onChange={props.openTab}>
|
<Tabs value={activeTab()} onChange={openTab}>
|
||||||
<div class="sticky top-0 shrink-0 flex">
|
<div class="sticky top-0 shrink-0 flex">
|
||||||
<Tabs.List
|
<Tabs.List
|
||||||
ref={(el: HTMLDivElement) => {
|
ref={(el: HTMLDivElement) => {
|
||||||
const stop = createFileTabListSync({ el, contextOpen: props.contextOpen })
|
const stop = createFileTabListSync({ el, contextOpen })
|
||||||
onCleanup(stop)
|
onCleanup(stop)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={props.reviewTab}>
|
<Show when={reviewTab()}>
|
||||||
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
|
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<div>{props.language.t("session.tab.review")}</div>
|
<div>{language.t("session.tab.review")}</div>
|
||||||
<Show when={props.hasReview}>
|
<Show when={hasReview()}>
|
||||||
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
|
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
|
||||||
{props.reviewCount}
|
{reviewCount()}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.contextOpen()}>
|
<Show when={contextOpen()}>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="context"
|
value="context"
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip value={props.language.t("common.closeTab")} placement="bottom">
|
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="close-small"
|
icon="close-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
onClick={() => props.tabs().close("context")}
|
onClick={() => tabs().close("context")}
|
||||||
aria-label={props.language.t("common.closeTab")}
|
aria-label={language.t("common.closeTab")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
onMiddleClick={() => props.tabs().close("context")}
|
onMiddleClick={() => tabs().close("context")}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<SessionContextUsage variant="indicator" />
|
<SessionContextUsage variant="indicator" />
|
||||||
<div>{props.language.t("session.tab.context")}</div>
|
<div>{language.t("session.tab.context")}</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Show>
|
</Show>
|
||||||
<SortableProvider ids={openedTabs()}>
|
<SortableProvider ids={openedTabs()}>
|
||||||
<For each={openedTabs()}>
|
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
|
||||||
{(tab) => <SortableTab tab={tab} onTabClose={props.tabs().close} />}
|
|
||||||
</For>
|
|
||||||
</SortableProvider>
|
</SortableProvider>
|
||||||
<StickyAddButton>
|
<StickyAddButton>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
title={props.language.t("command.file.open")}
|
title={language.t("command.file.open")}
|
||||||
keybind={props.command.keybind("file.open")}
|
keybind={command.keybind("file.open")}
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -158,72 +270,52 @@ export function SessionSidePanel(props: {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
iconSize="large"
|
iconSize="large"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.dialog.show(() => (
|
dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
||||||
<DialogSelectFile mode="files" onOpenFile={props.showAllFiles} />
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
aria-label={props.language.t("command.file.open")}
|
aria-label={language.t("command.file.open")}
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</StickyAddButton>
|
</StickyAddButton>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={props.reviewTab}>
|
<Show when={reviewTab()}>
|
||||||
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
<Show when={props.activeTab() === "review"}>{props.reviewPanel()}</Show>
|
<Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
<Show when={props.activeTab() === "empty"}>
|
<Show when={activeTab() === "empty"}>
|
||||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
|
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
|
||||||
<Mark class="w-14 opacity-10" />
|
<Mark class="w-14 opacity-10" />
|
||||||
<div class="text-14-regular text-text-weak max-w-56">
|
<div class="text-14-regular text-text-weak max-w-56">
|
||||||
{props.language.t("session.files.selectToOpen")}
|
{language.t("session.files.selectToOpen")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
||||||
<Show when={props.contextOpen()}>
|
<Show when={contextOpen()}>
|
||||||
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
<Show when={props.activeTab() === "context"}>
|
<Show when={activeTab() === "context"}>
|
||||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
<SessionContextTab
|
<SessionContextTab />
|
||||||
messages={props.vm.messages}
|
|
||||||
visibleUserMessages={props.vm.visibleUserMessages}
|
|
||||||
view={props.vm.view}
|
|
||||||
info={props.vm.info}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.activeFileTab()} keyed>
|
<Show when={activeFileTab()} keyed>
|
||||||
{(tab) => (
|
{(tab) => <FileTabContent tab={tab} />}
|
||||||
<FileTabContent
|
|
||||||
tab={tab}
|
|
||||||
activeTab={props.activeTab}
|
|
||||||
tabs={props.tabs}
|
|
||||||
view={props.vm.view}
|
|
||||||
handoffFiles={props.handoffFiles}
|
|
||||||
file={props.file}
|
|
||||||
comments={props.comments}
|
|
||||||
language={props.language}
|
|
||||||
codeComponent={props.codeComponent}
|
|
||||||
addCommentToContext={props.addCommentToContext}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
<Show when={props.activeDraggable()}>
|
<Show when={store.activeDraggable} keyed>
|
||||||
{(tab) => {
|
{(tab) => {
|
||||||
const path = createMemo(() => props.file.pathFromTab(tab()))
|
const path = createMemo(() => file.pathFromTab(tab))
|
||||||
return (
|
return (
|
||||||
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
|
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
|
||||||
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
|
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
|
||||||
@@ -240,50 +332,44 @@ export function SessionSidePanel(props: {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.layout.fileTree.opened()}>
|
<Show when={layout.fileTree.opened()}>
|
||||||
<div
|
<div id="file-tree-panel" class="relative shrink-0 h-full" style={{ width: `${layout.fileTree.width()}px` }}>
|
||||||
id="file-tree-panel"
|
|
||||||
class="relative shrink-0 h-full"
|
|
||||||
style={{ width: `${props.layout.fileTree.width()}px` }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="h-full flex flex-col overflow-hidden group/filetree"
|
class="h-full flex flex-col overflow-hidden group/filetree"
|
||||||
classList={{ "border-l border-border-weak-base": props.reviewOpen }}
|
classList={{ "border-l border-border-weak-base": reviewOpen() }}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
variant="pill"
|
variant="pill"
|
||||||
value={props.fileTreeTab()}
|
value={fileTreeTab()}
|
||||||
onChange={props.setFileTreeTabValue}
|
onChange={setFileTreeTabValue}
|
||||||
class="h-full"
|
class="h-full"
|
||||||
data-scope="filetree"
|
data-scope="filetree"
|
||||||
>
|
>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
||||||
{props.reviewCount}{" "}
|
{reviewCount()}{" "}
|
||||||
{props.language.t(
|
{language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
|
||||||
props.reviewCount === 1 ? "session.review.change.one" : "session.review.change.other",
|
|
||||||
)}
|
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
||||||
{props.language.t("session.files.all")}
|
{language.t("session.files.all")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Content value="changes" class="bg-background-base px-3 py-0">
|
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.hasReview}>
|
<Match when={hasReview()}>
|
||||||
<Show
|
<Show
|
||||||
when={props.diffsReady}
|
when={diffsReady()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||||
{props.language.t("common.loading")}
|
{language.t("common.loading")}
|
||||||
{props.language.t("common.loading.ellipsis")}
|
{language.t("common.loading.ellipsis")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FileTree
|
<FileTree
|
||||||
path=""
|
path=""
|
||||||
allowed={props.diffFiles}
|
allowed={diffFiles()}
|
||||||
kinds={props.kinds}
|
kinds={kinds()}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
active={props.activeDiff}
|
active={props.activeDiff}
|
||||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||||
@@ -292,17 +378,17 @@ export function SessionSidePanel(props: {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<div class="mt-8 text-center text-12-regular text-text-weak">
|
<div class="mt-8 text-center text-12-regular text-text-weak">
|
||||||
{props.language.t("session.review.noChanges")}
|
{language.t("session.review.noChanges")}
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="all" class="bg-background-base px-3 py-0">
|
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
||||||
<FileTree
|
<FileTree
|
||||||
path=""
|
path=""
|
||||||
modified={props.diffFiles}
|
modified={diffFiles()}
|
||||||
kinds={props.kinds}
|
kinds={kinds()}
|
||||||
onFileClick={(node) => props.openTab(props.file.tab(node.path))}
|
onFileClick={(node) => openTab(file.tab(node.path))}
|
||||||
/>
|
/>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -310,12 +396,12 @@ export function SessionSidePanel(props: {
|
|||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
edge="start"
|
edge="start"
|
||||||
size={props.layout.fileTree.width()}
|
size={layout.fileTree.width()}
|
||||||
min={200}
|
min={200}
|
||||||
max={480}
|
max={480}
|
||||||
collapseThreshold={160}
|
collapseThreshold={160}
|
||||||
onResize={props.layout.fileTree.resize}
|
onResize={layout.fileTree.resize}
|
||||||
onCollapse={props.layout.fileTree.close}
|
onCollapse={layout.fileTree.close}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,61 +1,161 @@
|
|||||||
import { For, Show, createMemo } from "solid-js"
|
import { For, Show, createEffect, createMemo, on } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
|
import { useParams } from "@solidjs/router"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
||||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||||
import { ConstrainDragYAxis } from "@/utils/solid-dnd"
|
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
|
|
||||||
import { SortableTerminalTab } from "@/components/session"
|
import { SortableTerminalTab } from "@/components/session"
|
||||||
import { Terminal } from "@/components/terminal"
|
import { Terminal } from "@/components/terminal"
|
||||||
import { useTerminal } from "@/context/terminal"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||||
import { terminalTabLabel } from "@/pages/session/terminal-label"
|
import { terminalTabLabel } from "@/pages/session/terminal-label"
|
||||||
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
|
import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
|
||||||
|
|
||||||
export function TerminalPanel(props: {
|
export function TerminalPanel() {
|
||||||
open: boolean
|
const params = useParams()
|
||||||
height: number
|
const layout = useLayout()
|
||||||
resize: (value: number) => void
|
const terminal = useTerminal()
|
||||||
close: () => void
|
const language = useLanguage()
|
||||||
terminal: ReturnType<typeof useTerminal>
|
const command = useCommand()
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
command: ReturnType<typeof useCommand>
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
handoff: () => string[]
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
activeTerminalDraggable: () => string | undefined
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
handleTerminalDragStart: (event: unknown) => void
|
|
||||||
handleTerminalDragOver: (event: DragEvent) => void
|
const opened = createMemo(() => view().terminal.opened())
|
||||||
handleTerminalDragEnd: () => void
|
const open = createMemo(() => isDesktop() && opened())
|
||||||
onCloseTab: () => void
|
const height = createMemo(() => layout.terminal.height())
|
||||||
}) {
|
const close = () => view().terminal.close()
|
||||||
const all = createMemo(() => props.terminal.all())
|
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
autoCreated: false,
|
||||||
|
activeDraggable: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!opened()) {
|
||||||
|
setStore("autoCreated", false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!terminal.ready() || terminal.all().length !== 0 || store.autoCreated) return
|
||||||
|
terminal.new()
|
||||||
|
setStore("autoCreated", true)
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => terminal.all().length,
|
||||||
|
(count, prevCount) => {
|
||||||
|
if (prevCount !== undefined && prevCount > 0 && count === 0) {
|
||||||
|
if (opened()) view().terminal.toggle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => terminal.active(),
|
||||||
|
(activeId) => {
|
||||||
|
if (!activeId || !opened()) return
|
||||||
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
focusTerminalById(activeId)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const dir = params.dir
|
||||||
|
if (!dir) return
|
||||||
|
if (!terminal.ready()) return
|
||||||
|
language.locale()
|
||||||
|
|
||||||
|
setTerminalHandoff(
|
||||||
|
dir,
|
||||||
|
terminal.all().map((pty) =>
|
||||||
|
terminalTabLabel({
|
||||||
|
title: pty.title,
|
||||||
|
titleNumber: pty.titleNumber,
|
||||||
|
t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handoff = createMemo(() => {
|
||||||
|
const dir = params.dir
|
||||||
|
if (!dir) return []
|
||||||
|
return getTerminalHandoff(dir) ?? []
|
||||||
|
})
|
||||||
|
|
||||||
|
const all = createMemo(() => terminal.all())
|
||||||
const ids = createMemo(() => all().map((pty) => pty.id))
|
const ids = createMemo(() => all().map((pty) => pty.id))
|
||||||
const byId = createMemo(() => new Map(all().map((pty) => [pty.id, pty])))
|
const byId = createMemo(() => new Map(all().map((pty) => [pty.id, pty])))
|
||||||
|
|
||||||
|
const handleTerminalDragStart = (event: unknown) => {
|
||||||
|
const id = getDraggableId(event)
|
||||||
|
if (!id) return
|
||||||
|
setStore("activeDraggable", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTerminalDragOver = (event: DragEvent) => {
|
||||||
|
const { draggable, droppable } = event
|
||||||
|
if (!draggable || !droppable) return
|
||||||
|
|
||||||
|
const terminals = terminal.all()
|
||||||
|
const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString())
|
||||||
|
const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString())
|
||||||
|
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
||||||
|
terminal.move(draggable.id.toString(), toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTerminalDragEnd = () => {
|
||||||
|
setStore("activeDraggable", undefined)
|
||||||
|
|
||||||
|
const activeId = terminal.active()
|
||||||
|
if (!activeId) return
|
||||||
|
setTimeout(() => {
|
||||||
|
focusTerminalById(activeId)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.open}>
|
<Show when={open()}>
|
||||||
<div
|
<div
|
||||||
id="terminal-panel"
|
id="terminal-panel"
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={props.language.t("terminal.title")}
|
aria-label={language.t("terminal.title")}
|
||||||
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
|
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
|
||||||
style={{ height: `${props.height}px` }}
|
style={{ height: `${height()}px` }}
|
||||||
>
|
>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
size={props.height}
|
size={height()}
|
||||||
min={100}
|
min={100}
|
||||||
max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6}
|
max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6}
|
||||||
collapseThreshold={50}
|
collapseThreshold={50}
|
||||||
onResize={props.resize}
|
onResize={layout.terminal.resize}
|
||||||
onCollapse={props.close}
|
onCollapse={close}
|
||||||
/>
|
/>
|
||||||
<Show
|
<Show
|
||||||
when={props.terminal.ready()}
|
when={terminal.ready()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="flex flex-col h-full pointer-events-none">
|
<div class="flex flex-col h-full pointer-events-none">
|
||||||
<div class="h-10 flex items-center gap-2 px-2 border-b border-border-weak-base bg-background-stronger overflow-hidden">
|
<div class="h-10 flex items-center gap-2 px-2 border-b border-border-weak-base bg-background-stronger overflow-hidden">
|
||||||
<For each={props.handoff()}>
|
<For each={handoff()}>
|
||||||
{(title) => (
|
{(title) => (
|
||||||
<div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
|
<div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
|
||||||
{title}
|
{title}
|
||||||
@@ -64,20 +164,18 @@ export function TerminalPanel(props: {
|
|||||||
</For>
|
</For>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<div class="text-text-weak pr-2">
|
<div class="text-text-weak pr-2">
|
||||||
{props.language.t("common.loading")}
|
{language.t("common.loading")}
|
||||||
{props.language.t("common.loading.ellipsis")}
|
{language.t("common.loading.ellipsis")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex items-center justify-center text-text-weak">
|
<div class="flex-1 flex items-center justify-center text-text-weak">{language.t("terminal.loading")}</div>
|
||||||
{props.language.t("terminal.loading")}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
onDragStart={props.handleTerminalDragStart}
|
onDragStart={handleTerminalDragStart}
|
||||||
onDragEnd={props.handleTerminalDragEnd}
|
onDragEnd={handleTerminalDragEnd}
|
||||||
onDragOver={props.handleTerminalDragOver}
|
onDragOver={handleTerminalDragOver}
|
||||||
collisionDetector={closestCenter}
|
collisionDetector={closestCenter}
|
||||||
>
|
>
|
||||||
<DragDropSensors />
|
<DragDropSensors />
|
||||||
@@ -85,36 +183,26 @@ export function TerminalPanel(props: {
|
|||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<Tabs
|
<Tabs
|
||||||
variant="alt"
|
variant="alt"
|
||||||
value={props.terminal.active()}
|
value={terminal.active()}
|
||||||
onChange={(id) => props.terminal.open(id)}
|
onChange={(id) => terminal.open(id)}
|
||||||
class="!h-auto !flex-none"
|
class="!h-auto !flex-none"
|
||||||
>
|
>
|
||||||
<Tabs.List class="h-10">
|
<Tabs.List class="h-10">
|
||||||
<SortableProvider ids={ids()}>
|
<SortableProvider ids={ids()}>
|
||||||
<For each={all()}>
|
<For each={all()}>{(pty) => <SortableTerminalTab terminal={pty} onClose={close} />}</For>
|
||||||
{(pty) => (
|
|
||||||
<SortableTerminalTab
|
|
||||||
terminal={pty}
|
|
||||||
onClose={() => {
|
|
||||||
props.close()
|
|
||||||
props.onCloseTab()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</SortableProvider>
|
</SortableProvider>
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
title={props.language.t("command.terminal.new")}
|
title={language.t("command.terminal.new")}
|
||||||
keybind={props.command.keybind("terminal.new")}
|
keybind={command.keybind("terminal.new")}
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
iconSize="large"
|
iconSize="large"
|
||||||
onClick={props.terminal.new}
|
onClick={terminal.new}
|
||||||
aria-label={props.language.t("command.terminal.new")}
|
aria-label={language.t("command.terminal.new")}
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,15 +215,11 @@ export function TerminalPanel(props: {
|
|||||||
id={`terminal-wrapper-${pty.id}`}
|
id={`terminal-wrapper-${pty.id}`}
|
||||||
class="absolute inset-0"
|
class="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
display: props.terminal.active() === pty.id ? "block" : "none",
|
display: terminal.active() === pty.id ? "block" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={pty.id} keyed>
|
<Show when={pty.id} keyed>
|
||||||
<Terminal
|
<Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
|
||||||
pty={pty}
|
|
||||||
onCleanup={props.terminal.update}
|
|
||||||
onConnectError={() => props.terminal.clone(pty.id)}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -143,25 +227,20 @@ export function TerminalPanel(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
<Show when={props.activeTerminalDraggable()}>
|
<Show when={store.activeDraggable}>
|
||||||
{(draggedId) => {
|
{(draggedId) => (
|
||||||
return (
|
<Show when={byId().get(draggedId())}>
|
||||||
<Show when={byId().get(draggedId())}>
|
{(t) => (
|
||||||
{(t) => (
|
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
|
||||||
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
|
{terminalTabLabel({
|
||||||
{terminalTabLabel({
|
title: t().title,
|
||||||
title: t().title,
|
titleNumber: t().titleNumber,
|
||||||
titleNumber: t().titleNumber,
|
t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
|
||||||
t: props.language.t as (
|
})}
|
||||||
key: string,
|
</div>
|
||||||
vars?: Record<string, string | number | boolean>,
|
)}
|
||||||
) => string,
|
</Show>
|
||||||
})}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Show>
|
</Show>
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
|
|||||||
@@ -22,29 +22,8 @@ import { UserMessage } from "@opencode-ai/sdk/v2"
|
|||||||
import { canAddSelectionContext } from "@/pages/session/session-command-helpers"
|
import { canAddSelectionContext } from "@/pages/session/session-command-helpers"
|
||||||
|
|
||||||
export type SessionCommandContext = {
|
export type SessionCommandContext = {
|
||||||
command: ReturnType<typeof useCommand>
|
|
||||||
dialog: ReturnType<typeof useDialog>
|
|
||||||
file: ReturnType<typeof useFile>
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
local: ReturnType<typeof useLocal>
|
|
||||||
permission: ReturnType<typeof usePermission>
|
|
||||||
prompt: ReturnType<typeof usePrompt>
|
|
||||||
sdk: ReturnType<typeof useSDK>
|
|
||||||
sync: ReturnType<typeof useSync>
|
|
||||||
terminal: ReturnType<typeof useTerminal>
|
|
||||||
layout: ReturnType<typeof useLayout>
|
|
||||||
params: ReturnType<typeof useParams>
|
|
||||||
navigate: ReturnType<typeof useNavigate>
|
|
||||||
tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
|
||||||
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
|
||||||
info: () => { revert?: { messageID?: string }; share?: { url?: string } } | undefined
|
|
||||||
status: () => { type: string }
|
|
||||||
userMessages: () => UserMessage[]
|
|
||||||
visibleUserMessages: () => UserMessage[]
|
|
||||||
showAllFiles: () => void
|
|
||||||
navigateMessageByOffset: (offset: number) => void
|
navigateMessageByOffset: (offset: number) => void
|
||||||
setActiveMessage: (message: UserMessage | undefined) => void
|
setActiveMessage: (message: UserMessage | undefined) => void
|
||||||
addSelectionToContext: (path: string, selection: FileSelection) => void
|
|
||||||
focusInput: () => void
|
focusInput: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,45 +34,98 @@ const withCategory = (category: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSessionCommands = (input: SessionCommandContext) => {
|
export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
const sessionCommand = withCategory(input.language.t("command.category.session"))
|
const command = useCommand()
|
||||||
const fileCommand = withCategory(input.language.t("command.category.file"))
|
const dialog = useDialog()
|
||||||
const contextCommand = withCategory(input.language.t("command.category.context"))
|
const file = useFile()
|
||||||
const viewCommand = withCategory(input.language.t("command.category.view"))
|
const language = useLanguage()
|
||||||
const terminalCommand = withCategory(input.language.t("command.category.terminal"))
|
const local = useLocal()
|
||||||
const modelCommand = withCategory(input.language.t("command.category.model"))
|
const permission = usePermission()
|
||||||
const mcpCommand = withCategory(input.language.t("command.category.mcp"))
|
const prompt = usePrompt()
|
||||||
const agentCommand = withCategory(input.language.t("command.category.agent"))
|
const sdk = useSDK()
|
||||||
const permissionsCommand = withCategory(input.language.t("command.category.permissions"))
|
const sync = useSync()
|
||||||
|
const terminal = useTerminal()
|
||||||
|
const layout = useLayout()
|
||||||
|
const params = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
|
|
||||||
|
const idle = { type: "idle" as const }
|
||||||
|
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
||||||
|
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||||
|
const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[])
|
||||||
|
const visibleUserMessages = createMemo(() => {
|
||||||
|
const revert = info()?.revert?.messageID
|
||||||
|
if (!revert) return userMessages()
|
||||||
|
return userMessages().filter((m) => m.id < revert)
|
||||||
|
})
|
||||||
|
|
||||||
|
const showAllFiles = () => {
|
||||||
|
if (layout.fileTree.tab() !== "changes") return
|
||||||
|
layout.fileTree.setTab("all")
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionPreview = (path: string, selection: FileSelection) => {
|
||||||
|
const content = file.get(path)?.content?.content
|
||||||
|
if (!content) return undefined
|
||||||
|
const start = Math.max(1, Math.min(selection.startLine, selection.endLine))
|
||||||
|
const end = Math.max(selection.startLine, selection.endLine)
|
||||||
|
const lines = content.split("\n").slice(start - 1, end)
|
||||||
|
if (lines.length === 0) return undefined
|
||||||
|
return lines.slice(0, 2).join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSelectionToContext = (path: string, selection: FileSelection) => {
|
||||||
|
const preview = selectionPreview(path, selection)
|
||||||
|
prompt.context.add({ type: "file", path, selection, preview })
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateMessageByOffset = actions.navigateMessageByOffset
|
||||||
|
const setActiveMessage = actions.setActiveMessage
|
||||||
|
const focusInput = actions.focusInput
|
||||||
|
|
||||||
|
const sessionCommand = withCategory(language.t("command.category.session"))
|
||||||
|
const fileCommand = withCategory(language.t("command.category.file"))
|
||||||
|
const contextCommand = withCategory(language.t("command.category.context"))
|
||||||
|
const viewCommand = withCategory(language.t("command.category.view"))
|
||||||
|
const terminalCommand = withCategory(language.t("command.category.terminal"))
|
||||||
|
const modelCommand = withCategory(language.t("command.category.model"))
|
||||||
|
const mcpCommand = withCategory(language.t("command.category.mcp"))
|
||||||
|
const agentCommand = withCategory(language.t("command.category.agent"))
|
||||||
|
const permissionsCommand = withCategory(language.t("command.category.permissions"))
|
||||||
|
|
||||||
const sessionCommands = createMemo(() => [
|
const sessionCommands = createMemo(() => [
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "session.new",
|
id: "session.new",
|
||||||
title: input.language.t("command.session.new"),
|
title: language.t("command.session.new"),
|
||||||
keybind: "mod+shift+s",
|
keybind: "mod+shift+s",
|
||||||
slash: "new",
|
slash: "new",
|
||||||
onSelect: () => input.navigate(`/${input.params.dir}/session`),
|
onSelect: () => navigate(`/${params.dir}/session`),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const fileCommands = createMemo(() => [
|
const fileCommands = createMemo(() => [
|
||||||
fileCommand({
|
fileCommand({
|
||||||
id: "file.open",
|
id: "file.open",
|
||||||
title: input.language.t("command.file.open"),
|
title: language.t("command.file.open"),
|
||||||
description: input.language.t("palette.search.placeholder"),
|
description: language.t("palette.search.placeholder"),
|
||||||
keybind: "mod+p",
|
keybind: "mod+p",
|
||||||
slash: "open",
|
slash: "open",
|
||||||
onSelect: () => input.dialog.show(() => <DialogSelectFile onOpenFile={input.showAllFiles} />),
|
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
||||||
}),
|
}),
|
||||||
fileCommand({
|
fileCommand({
|
||||||
id: "tab.close",
|
id: "tab.close",
|
||||||
title: input.language.t("command.tab.close"),
|
title: language.t("command.tab.close"),
|
||||||
keybind: "mod+w",
|
keybind: "mod+w",
|
||||||
disabled: !input.tabs().active(),
|
disabled: !tabs().active(),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
const active = input.tabs().active()
|
const active = tabs().active()
|
||||||
if (!active) return
|
if (!active) return
|
||||||
input.tabs().close(active)
|
tabs().close(active)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -101,30 +133,30 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
const contextCommands = createMemo(() => [
|
const contextCommands = createMemo(() => [
|
||||||
contextCommand({
|
contextCommand({
|
||||||
id: "context.addSelection",
|
id: "context.addSelection",
|
||||||
title: input.language.t("command.context.addSelection"),
|
title: language.t("command.context.addSelection"),
|
||||||
description: input.language.t("command.context.addSelection.description"),
|
description: language.t("command.context.addSelection.description"),
|
||||||
keybind: "mod+shift+l",
|
keybind: "mod+shift+l",
|
||||||
disabled: !canAddSelectionContext({
|
disabled: !canAddSelectionContext({
|
||||||
active: input.tabs().active(),
|
active: tabs().active(),
|
||||||
pathFromTab: input.file.pathFromTab,
|
pathFromTab: file.pathFromTab,
|
||||||
selectedLines: input.file.selectedLines,
|
selectedLines: file.selectedLines,
|
||||||
}),
|
}),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
const active = input.tabs().active()
|
const active = tabs().active()
|
||||||
if (!active) return
|
if (!active) return
|
||||||
const path = input.file.pathFromTab(active)
|
const path = file.pathFromTab(active)
|
||||||
if (!path) return
|
if (!path) return
|
||||||
|
|
||||||
const range = input.file.selectedLines(path) as SelectedLineRange | null | undefined
|
const range = file.selectedLines(path) as SelectedLineRange | null | undefined
|
||||||
if (!range) {
|
if (!range) {
|
||||||
showToast({
|
showToast({
|
||||||
title: input.language.t("toast.context.noLineSelection.title"),
|
title: language.t("toast.context.noLineSelection.title"),
|
||||||
description: input.language.t("toast.context.noLineSelection.description"),
|
description: language.t("toast.context.noLineSelection.description"),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
input.addSelectionToContext(path, selectionFromLines(range))
|
addSelectionToContext(path, selectionFromLines(range))
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -132,37 +164,37 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
const viewCommands = createMemo(() => [
|
const viewCommands = createMemo(() => [
|
||||||
viewCommand({
|
viewCommand({
|
||||||
id: "terminal.toggle",
|
id: "terminal.toggle",
|
||||||
title: input.language.t("command.terminal.toggle"),
|
title: language.t("command.terminal.toggle"),
|
||||||
keybind: "ctrl+`",
|
keybind: "ctrl+`",
|
||||||
slash: "terminal",
|
slash: "terminal",
|
||||||
onSelect: () => input.view().terminal.toggle(),
|
onSelect: () => view().terminal.toggle(),
|
||||||
}),
|
}),
|
||||||
viewCommand({
|
viewCommand({
|
||||||
id: "review.toggle",
|
id: "review.toggle",
|
||||||
title: input.language.t("command.review.toggle"),
|
title: language.t("command.review.toggle"),
|
||||||
keybind: "mod+shift+r",
|
keybind: "mod+shift+r",
|
||||||
onSelect: () => input.view().reviewPanel.toggle(),
|
onSelect: () => view().reviewPanel.toggle(),
|
||||||
}),
|
}),
|
||||||
viewCommand({
|
viewCommand({
|
||||||
id: "fileTree.toggle",
|
id: "fileTree.toggle",
|
||||||
title: input.language.t("command.fileTree.toggle"),
|
title: language.t("command.fileTree.toggle"),
|
||||||
keybind: "mod+\\",
|
keybind: "mod+\\",
|
||||||
onSelect: () => input.layout.fileTree.toggle(),
|
onSelect: () => layout.fileTree.toggle(),
|
||||||
}),
|
}),
|
||||||
viewCommand({
|
viewCommand({
|
||||||
id: "input.focus",
|
id: "input.focus",
|
||||||
title: input.language.t("command.input.focus"),
|
title: language.t("command.input.focus"),
|
||||||
keybind: "ctrl+l",
|
keybind: "ctrl+l",
|
||||||
onSelect: () => input.focusInput(),
|
onSelect: () => focusInput(),
|
||||||
}),
|
}),
|
||||||
terminalCommand({
|
terminalCommand({
|
||||||
id: "terminal.new",
|
id: "terminal.new",
|
||||||
title: input.language.t("command.terminal.new"),
|
title: language.t("command.terminal.new"),
|
||||||
description: input.language.t("command.terminal.new.description"),
|
description: language.t("command.terminal.new.description"),
|
||||||
keybind: "ctrl+alt+t",
|
keybind: "ctrl+alt+t",
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
if (input.terminal.all().length > 0) input.terminal.new()
|
if (terminal.all().length > 0) terminal.new()
|
||||||
input.view().terminal.open()
|
view().terminal.open()
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -170,61 +202,61 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
const messageCommands = createMemo(() => [
|
const messageCommands = createMemo(() => [
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "message.previous",
|
id: "message.previous",
|
||||||
title: input.language.t("command.message.previous"),
|
title: language.t("command.message.previous"),
|
||||||
description: input.language.t("command.message.previous.description"),
|
description: language.t("command.message.previous.description"),
|
||||||
keybind: "mod+arrowup",
|
keybind: "mod+arrowup",
|
||||||
disabled: !input.params.id,
|
disabled: !params.id,
|
||||||
onSelect: () => input.navigateMessageByOffset(-1),
|
onSelect: () => navigateMessageByOffset(-1),
|
||||||
}),
|
}),
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "message.next",
|
id: "message.next",
|
||||||
title: input.language.t("command.message.next"),
|
title: language.t("command.message.next"),
|
||||||
description: input.language.t("command.message.next.description"),
|
description: language.t("command.message.next.description"),
|
||||||
keybind: "mod+arrowdown",
|
keybind: "mod+arrowdown",
|
||||||
disabled: !input.params.id,
|
disabled: !params.id,
|
||||||
onSelect: () => input.navigateMessageByOffset(1),
|
onSelect: () => navigateMessageByOffset(1),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const agentCommands = createMemo(() => [
|
const agentCommands = createMemo(() => [
|
||||||
modelCommand({
|
modelCommand({
|
||||||
id: "model.choose",
|
id: "model.choose",
|
||||||
title: input.language.t("command.model.choose"),
|
title: language.t("command.model.choose"),
|
||||||
description: input.language.t("command.model.choose.description"),
|
description: language.t("command.model.choose.description"),
|
||||||
keybind: "mod+'",
|
keybind: "mod+'",
|
||||||
slash: "model",
|
slash: "model",
|
||||||
onSelect: () => input.dialog.show(() => <DialogSelectModel />),
|
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
||||||
}),
|
}),
|
||||||
mcpCommand({
|
mcpCommand({
|
||||||
id: "mcp.toggle",
|
id: "mcp.toggle",
|
||||||
title: input.language.t("command.mcp.toggle"),
|
title: language.t("command.mcp.toggle"),
|
||||||
description: input.language.t("command.mcp.toggle.description"),
|
description: language.t("command.mcp.toggle.description"),
|
||||||
keybind: "mod+;",
|
keybind: "mod+;",
|
||||||
slash: "mcp",
|
slash: "mcp",
|
||||||
onSelect: () => input.dialog.show(() => <DialogSelectMcp />),
|
onSelect: () => dialog.show(() => <DialogSelectMcp />),
|
||||||
}),
|
}),
|
||||||
agentCommand({
|
agentCommand({
|
||||||
id: "agent.cycle",
|
id: "agent.cycle",
|
||||||
title: input.language.t("command.agent.cycle"),
|
title: language.t("command.agent.cycle"),
|
||||||
description: input.language.t("command.agent.cycle.description"),
|
description: language.t("command.agent.cycle.description"),
|
||||||
keybind: "mod+.",
|
keybind: "mod+.",
|
||||||
slash: "agent",
|
slash: "agent",
|
||||||
onSelect: () => input.local.agent.move(1),
|
onSelect: () => local.agent.move(1),
|
||||||
}),
|
}),
|
||||||
agentCommand({
|
agentCommand({
|
||||||
id: "agent.cycle.reverse",
|
id: "agent.cycle.reverse",
|
||||||
title: input.language.t("command.agent.cycle.reverse"),
|
title: language.t("command.agent.cycle.reverse"),
|
||||||
description: input.language.t("command.agent.cycle.reverse.description"),
|
description: language.t("command.agent.cycle.reverse.description"),
|
||||||
keybind: "shift+mod+.",
|
keybind: "shift+mod+.",
|
||||||
onSelect: () => input.local.agent.move(-1),
|
onSelect: () => local.agent.move(-1),
|
||||||
}),
|
}),
|
||||||
modelCommand({
|
modelCommand({
|
||||||
id: "model.variant.cycle",
|
id: "model.variant.cycle",
|
||||||
title: input.language.t("command.model.variant.cycle"),
|
title: language.t("command.model.variant.cycle"),
|
||||||
description: input.language.t("command.model.variant.cycle.description"),
|
description: language.t("command.model.variant.cycle.description"),
|
||||||
keybind: "shift+mod+d",
|
keybind: "shift+mod+d",
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
input.local.model.variant.cycle()
|
local.model.variant.cycle()
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -233,22 +265,22 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
permissionsCommand({
|
permissionsCommand({
|
||||||
id: "permissions.autoaccept",
|
id: "permissions.autoaccept",
|
||||||
title:
|
title:
|
||||||
input.params.id && input.permission.isAutoAccepting(input.params.id, input.sdk.directory)
|
params.id && permission.isAutoAccepting(params.id, sdk.directory)
|
||||||
? input.language.t("command.permissions.autoaccept.disable")
|
? language.t("command.permissions.autoaccept.disable")
|
||||||
: input.language.t("command.permissions.autoaccept.enable"),
|
: language.t("command.permissions.autoaccept.enable"),
|
||||||
keybind: "mod+shift+a",
|
keybind: "mod+shift+a",
|
||||||
disabled: !input.params.id || !input.permission.permissionsEnabled(),
|
disabled: !params.id || !permission.permissionsEnabled(),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
const sessionID = input.params.id
|
const sessionID = params.id
|
||||||
if (!sessionID) return
|
if (!sessionID) return
|
||||||
input.permission.toggleAutoAccept(sessionID, input.sdk.directory)
|
permission.toggleAutoAccept(sessionID, sdk.directory)
|
||||||
showToast({
|
showToast({
|
||||||
title: input.permission.isAutoAccepting(sessionID, input.sdk.directory)
|
title: permission.isAutoAccepting(sessionID, sdk.directory)
|
||||||
? input.language.t("toast.permissions.autoaccept.on.title")
|
? language.t("toast.permissions.autoaccept.on.title")
|
||||||
: input.language.t("toast.permissions.autoaccept.off.title"),
|
: language.t("toast.permissions.autoaccept.off.title"),
|
||||||
description: input.permission.isAutoAccepting(sessionID, input.sdk.directory)
|
description: permission.isAutoAccepting(sessionID, sdk.directory)
|
||||||
? input.language.t("toast.permissions.autoaccept.on.description")
|
? language.t("toast.permissions.autoaccept.on.description")
|
||||||
: input.language.t("toast.permissions.autoaccept.off.description"),
|
: language.t("toast.permissions.autoaccept.off.description"),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -257,71 +289,71 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
const sessionActionCommands = createMemo(() => [
|
const sessionActionCommands = createMemo(() => [
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "session.undo",
|
id: "session.undo",
|
||||||
title: input.language.t("command.session.undo"),
|
title: language.t("command.session.undo"),
|
||||||
description: input.language.t("command.session.undo.description"),
|
description: language.t("command.session.undo.description"),
|
||||||
slash: "undo",
|
slash: "undo",
|
||||||
disabled: !input.params.id || input.visibleUserMessages().length === 0,
|
disabled: !params.id || visibleUserMessages().length === 0,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
const sessionID = input.params.id
|
const sessionID = params.id
|
||||||
if (!sessionID) return
|
if (!sessionID) return
|
||||||
if (input.status()?.type !== "idle") {
|
if (status()?.type !== "idle") {
|
||||||
await input.sdk.client.session.abort({ sessionID }).catch(() => {})
|
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||||
}
|
}
|
||||||
const revert = input.info()?.revert?.messageID
|
const revert = info()?.revert?.messageID
|
||||||
const message = findLast(input.userMessages(), (x) => !revert || x.id < revert)
|
const message = findLast(userMessages(), (x) => !revert || x.id < revert)
|
||||||
if (!message) return
|
if (!message) return
|
||||||
await input.sdk.client.session.revert({ sessionID, messageID: message.id })
|
await sdk.client.session.revert({ sessionID, messageID: message.id })
|
||||||
const parts = input.sync.data.part[message.id]
|
const parts = sync.data.part[message.id]
|
||||||
if (parts) {
|
if (parts) {
|
||||||
const restored = extractPromptFromParts(parts, { directory: input.sdk.directory })
|
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
||||||
input.prompt.set(restored)
|
prompt.set(restored)
|
||||||
}
|
}
|
||||||
const priorMessage = findLast(input.userMessages(), (x) => x.id < message.id)
|
const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
|
||||||
input.setActiveMessage(priorMessage)
|
setActiveMessage(priorMessage)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "session.redo",
|
id: "session.redo",
|
||||||
title: input.language.t("command.session.redo"),
|
title: language.t("command.session.redo"),
|
||||||
description: input.language.t("command.session.redo.description"),
|
description: language.t("command.session.redo.description"),
|
||||||
slash: "redo",
|
slash: "redo",
|
||||||
disabled: !input.params.id || !input.info()?.revert?.messageID,
|
disabled: !params.id || !info()?.revert?.messageID,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
const sessionID = input.params.id
|
const sessionID = params.id
|
||||||
if (!sessionID) return
|
if (!sessionID) return
|
||||||
const revertMessageID = input.info()?.revert?.messageID
|
const revertMessageID = info()?.revert?.messageID
|
||||||
if (!revertMessageID) return
|
if (!revertMessageID) return
|
||||||
const nextMessage = input.userMessages().find((x) => x.id > revertMessageID)
|
const nextMessage = userMessages().find((x) => x.id > revertMessageID)
|
||||||
if (!nextMessage) {
|
if (!nextMessage) {
|
||||||
await input.sdk.client.session.unrevert({ sessionID })
|
await sdk.client.session.unrevert({ sessionID })
|
||||||
input.prompt.reset()
|
prompt.reset()
|
||||||
const lastMsg = findLast(input.userMessages(), (x) => x.id >= revertMessageID)
|
const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID)
|
||||||
input.setActiveMessage(lastMsg)
|
setActiveMessage(lastMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await input.sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
||||||
const priorMsg = findLast(input.userMessages(), (x) => x.id < nextMessage.id)
|
const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
|
||||||
input.setActiveMessage(priorMsg)
|
setActiveMessage(priorMsg)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "session.compact",
|
id: "session.compact",
|
||||||
title: input.language.t("command.session.compact"),
|
title: language.t("command.session.compact"),
|
||||||
description: input.language.t("command.session.compact.description"),
|
description: language.t("command.session.compact.description"),
|
||||||
slash: "compact",
|
slash: "compact",
|
||||||
disabled: !input.params.id || input.visibleUserMessages().length === 0,
|
disabled: !params.id || visibleUserMessages().length === 0,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
const sessionID = input.params.id
|
const sessionID = params.id
|
||||||
if (!sessionID) return
|
if (!sessionID) return
|
||||||
const model = input.local.model.current()
|
const model = local.model.current()
|
||||||
if (!model) {
|
if (!model) {
|
||||||
showToast({
|
showToast({
|
||||||
title: input.language.t("toast.model.none.title"),
|
title: language.t("toast.model.none.title"),
|
||||||
description: input.language.t("toast.model.none.description"),
|
description: language.t("toast.model.none.description"),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await input.sdk.client.session.summarize({
|
await sdk.client.session.summarize({
|
||||||
sessionID,
|
sessionID,
|
||||||
modelID: model.id,
|
modelID: model.id,
|
||||||
providerID: model.provider.id,
|
providerID: model.provider.id,
|
||||||
@@ -330,29 +362,27 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
}),
|
}),
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "session.fork",
|
id: "session.fork",
|
||||||
title: input.language.t("command.session.fork"),
|
title: language.t("command.session.fork"),
|
||||||
description: input.language.t("command.session.fork.description"),
|
description: language.t("command.session.fork.description"),
|
||||||
slash: "fork",
|
slash: "fork",
|
||||||
disabled: !input.params.id || input.visibleUserMessages().length === 0,
|
disabled: !params.id || visibleUserMessages().length === 0,
|
||||||
onSelect: () => input.dialog.show(() => <DialogFork />),
|
onSelect: () => dialog.show(() => <DialogFork />),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const shareCommands = createMemo(() => {
|
const shareCommands = createMemo(() => {
|
||||||
if (input.sync.data.config.share === "disabled") return []
|
if (sync.data.config.share === "disabled") return []
|
||||||
return [
|
return [
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "session.share",
|
id: "session.share",
|
||||||
title: input.info()?.share?.url
|
title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"),
|
||||||
? input.language.t("session.share.copy.copyLink")
|
description: info()?.share?.url
|
||||||
: input.language.t("command.session.share"),
|
? language.t("toast.session.share.success.description")
|
||||||
description: input.info()?.share?.url
|
: language.t("command.session.share.description"),
|
||||||
? input.language.t("toast.session.share.success.description")
|
|
||||||
: input.language.t("command.session.share.description"),
|
|
||||||
slash: "share",
|
slash: "share",
|
||||||
disabled: !input.params.id,
|
disabled: !params.id,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
if (!input.params.id) return
|
if (!params.id) return
|
||||||
|
|
||||||
const write = (value: string) => {
|
const write = (value: string) => {
|
||||||
const body = typeof document === "undefined" ? undefined : document.body
|
const body = typeof document === "undefined" ? undefined : document.body
|
||||||
@@ -382,7 +412,7 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
const ok = await write(url)
|
const ok = await write(url)
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
showToast({
|
showToast({
|
||||||
title: input.language.t("toast.session.share.copyFailed.title"),
|
title: language.t("toast.session.share.copyFailed.title"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -390,27 +420,27 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
|
|
||||||
showToast({
|
showToast({
|
||||||
title: existing
|
title: existing
|
||||||
? input.language.t("session.share.copy.copied")
|
? language.t("session.share.copy.copied")
|
||||||
: input.language.t("toast.session.share.success.title"),
|
: language.t("toast.session.share.success.title"),
|
||||||
description: input.language.t("toast.session.share.success.description"),
|
description: language.t("toast.session.share.success.description"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = input.info()?.share?.url
|
const existing = info()?.share?.url
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await copy(existing, true)
|
await copy(existing, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = await input.sdk.client.session
|
const url = await sdk.client.session
|
||||||
.share({ sessionID: input.params.id })
|
.share({ sessionID: params.id })
|
||||||
.then((res) => res.data?.share?.url)
|
.then((res) => res.data?.share?.url)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (!url) {
|
if (!url) {
|
||||||
showToast({
|
showToast({
|
||||||
title: input.language.t("toast.session.share.failed.title"),
|
title: language.t("toast.session.share.failed.title"),
|
||||||
description: input.language.t("toast.session.share.failed.description"),
|
description: language.t("toast.session.share.failed.description"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -421,25 +451,25 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
}),
|
}),
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "session.unshare",
|
id: "session.unshare",
|
||||||
title: input.language.t("command.session.unshare"),
|
title: language.t("command.session.unshare"),
|
||||||
description: input.language.t("command.session.unshare.description"),
|
description: language.t("command.session.unshare.description"),
|
||||||
slash: "unshare",
|
slash: "unshare",
|
||||||
disabled: !input.params.id || !input.info()?.share?.url,
|
disabled: !params.id || !info()?.share?.url,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
if (!input.params.id) return
|
if (!params.id) return
|
||||||
await input.sdk.client.session
|
await sdk.client.session
|
||||||
.unshare({ sessionID: input.params.id })
|
.unshare({ sessionID: params.id })
|
||||||
.then(() =>
|
.then(() =>
|
||||||
showToast({
|
showToast({
|
||||||
title: input.language.t("toast.session.unshare.success.title"),
|
title: language.t("toast.session.unshare.success.title"),
|
||||||
description: input.language.t("toast.session.unshare.success.description"),
|
description: language.t("toast.session.unshare.success.description"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
showToast({
|
showToast({
|
||||||
title: input.language.t("toast.session.unshare.failed.title"),
|
title: language.t("toast.session.unshare.failed.title"),
|
||||||
description: input.language.t("toast.session.unshare.failed.description"),
|
description: language.t("toast.session.unshare.failed.description"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -448,7 +478,7 @@ export const useSessionCommands = (input: SessionCommandContext) => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
input.command.register("session", () =>
|
command.register("session", () =>
|
||||||
[
|
[
|
||||||
sessionCommands(),
|
sessionCommands(),
|
||||||
fileCommands(),
|
fileCommands(),
|
||||||
|
|||||||
Vendored
+2
@@ -1,6 +1,8 @@
|
|||||||
/* This file is auto-generated by SST. Do not edit. */
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
|
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
import { uuid } from "@/utils/uuid"
|
|
||||||
|
|
||||||
type Nav = {
|
|
||||||
id: string
|
|
||||||
dir?: string
|
|
||||||
from?: string
|
|
||||||
to: string
|
|
||||||
trigger?: string
|
|
||||||
start: number
|
|
||||||
marks: Record<string, number>
|
|
||||||
logged: boolean
|
|
||||||
timer?: ReturnType<typeof setTimeout>
|
|
||||||
}
|
|
||||||
|
|
||||||
const dev = import.meta.env.DEV
|
|
||||||
|
|
||||||
const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`
|
|
||||||
|
|
||||||
const now = () => performance.now()
|
|
||||||
|
|
||||||
const navs = new Map<string, Nav>()
|
|
||||||
const pending = new Map<string, string>()
|
|
||||||
const active = new Map<string, string>()
|
|
||||||
|
|
||||||
const required = [
|
|
||||||
"session:params",
|
|
||||||
"session:data-ready",
|
|
||||||
"session:first-turn-mounted",
|
|
||||||
"storage:prompt-ready",
|
|
||||||
"storage:terminal-ready",
|
|
||||||
"storage:file-view-ready",
|
|
||||||
]
|
|
||||||
|
|
||||||
function flush(id: string, reason: "complete" | "timeout") {
|
|
||||||
if (!dev) return
|
|
||||||
const nav = navs.get(id)
|
|
||||||
if (!nav) return
|
|
||||||
if (nav.logged) return
|
|
||||||
|
|
||||||
nav.logged = true
|
|
||||||
if (nav.timer) clearTimeout(nav.timer)
|
|
||||||
|
|
||||||
const baseName = nav.marks["navigate:start"] !== undefined ? "navigate:start" : "session:params"
|
|
||||||
const base = nav.marks[baseName] ?? nav.start
|
|
||||||
|
|
||||||
const ms = Object.fromEntries(
|
|
||||||
Object.entries(nav.marks)
|
|
||||||
.slice()
|
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
|
||||||
.map(([name, t]) => [name, Math.round((t - base) * 100) / 100]),
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"perf.session-nav " +
|
|
||||||
JSON.stringify({
|
|
||||||
type: "perf.session-nav.v0",
|
|
||||||
id: nav.id,
|
|
||||||
dir: nav.dir,
|
|
||||||
from: nav.from,
|
|
||||||
to: nav.to,
|
|
||||||
trigger: nav.trigger,
|
|
||||||
base: baseName,
|
|
||||||
reason,
|
|
||||||
ms,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
navs.delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeFlush(id: string) {
|
|
||||||
if (!dev) return
|
|
||||||
const nav = navs.get(id)
|
|
||||||
if (!nav) return
|
|
||||||
if (nav.logged) return
|
|
||||||
if (!required.every((name) => nav.marks[name] !== undefined)) return
|
|
||||||
flush(id, "complete")
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensure(id: string, data: Omit<Nav, "marks" | "logged" | "timer">) {
|
|
||||||
const existing = navs.get(id)
|
|
||||||
if (existing) return existing
|
|
||||||
|
|
||||||
const nav: Nav = {
|
|
||||||
...data,
|
|
||||||
marks: {},
|
|
||||||
logged: false,
|
|
||||||
}
|
|
||||||
nav.timer = setTimeout(() => flush(id, "timeout"), 5000)
|
|
||||||
navs.set(id, nav)
|
|
||||||
return nav
|
|
||||||
}
|
|
||||||
|
|
||||||
export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) {
|
|
||||||
if (!dev) return
|
|
||||||
|
|
||||||
const id = uuid()
|
|
||||||
const start = now()
|
|
||||||
const nav = ensure(id, { ...input, id, start })
|
|
||||||
nav.marks["navigate:start"] = start
|
|
||||||
|
|
||||||
pending.set(key(input.dir, input.to), id)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
export function navParams(input: { dir?: string; from?: string; to: string }) {
|
|
||||||
if (!dev) return
|
|
||||||
|
|
||||||
const k = key(input.dir, input.to)
|
|
||||||
const pendingId = pending.get(k)
|
|
||||||
if (pendingId) pending.delete(k)
|
|
||||||
const id = pendingId ?? uuid()
|
|
||||||
|
|
||||||
const start = now()
|
|
||||||
const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })
|
|
||||||
nav.marks["session:params"] = start
|
|
||||||
|
|
||||||
active.set(k, id)
|
|
||||||
maybeFlush(id)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
export function navMark(input: { dir?: string; to: string; name: string }) {
|
|
||||||
if (!dev) return
|
|
||||||
|
|
||||||
const id = active.get(key(input.dir, input.to))
|
|
||||||
if (!id) return
|
|
||||||
|
|
||||||
const nav = navs.get(id)
|
|
||||||
if (!nav) return
|
|
||||||
if (nav.marks[input.name] !== undefined) return
|
|
||||||
|
|
||||||
nav.marks[input.name] = now()
|
|
||||||
maybeFlush(id)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { ServerConnection } from "@/context/server"
|
||||||
import { checkServerHealth } from "./server-health"
|
import { checkServerHealth } from "./server-health"
|
||||||
|
|
||||||
|
const server: ServerConnection.HttpBase = {
|
||||||
|
url: "http://localhost:4096",
|
||||||
|
}
|
||||||
|
|
||||||
function abortFromInput(input: RequestInfo | URL, init?: RequestInit) {
|
function abortFromInput(input: RequestInfo | URL, init?: RequestInit) {
|
||||||
if (init?.signal) return init.signal
|
if (init?.signal) return init.signal
|
||||||
if (input instanceof Request) return input.signal
|
if (input instanceof Request) return input.signal
|
||||||
@@ -15,7 +20,7 @@ describe("checkServerHealth", () => {
|
|||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
})) as unknown as typeof globalThis.fetch
|
})) as unknown as typeof globalThis.fetch
|
||||||
|
|
||||||
const result = await checkServerHealth("http://localhost:4096", fetch)
|
const result = await checkServerHealth(server, fetch)
|
||||||
|
|
||||||
expect(result).toEqual({ healthy: true, version: "1.2.3" })
|
expect(result).toEqual({ healthy: true, version: "1.2.3" })
|
||||||
})
|
})
|
||||||
@@ -25,7 +30,7 @@ describe("checkServerHealth", () => {
|
|||||||
throw new Error("network")
|
throw new Error("network")
|
||||||
}) as unknown as typeof globalThis.fetch
|
}) as unknown as typeof globalThis.fetch
|
||||||
|
|
||||||
const result = await checkServerHealth("http://localhost:4096", fetch)
|
const result = await checkServerHealth(server, fetch)
|
||||||
|
|
||||||
expect(result).toEqual({ healthy: false })
|
expect(result).toEqual({ healthy: false })
|
||||||
})
|
})
|
||||||
@@ -51,7 +56,9 @@ describe("checkServerHealth", () => {
|
|||||||
)
|
)
|
||||||
})) as unknown as typeof globalThis.fetch
|
})) as unknown as typeof globalThis.fetch
|
||||||
|
|
||||||
const result = await checkServerHealth("http://localhost:4096", fetch, { timeoutMs: 10 }).finally(() => {
|
const result = await checkServerHealth(server, fetch, {
|
||||||
|
timeoutMs: 10,
|
||||||
|
}).finally(() => {
|
||||||
if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
|
if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
|
||||||
if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
|
if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
|
||||||
})
|
})
|
||||||
@@ -71,7 +78,9 @@ describe("checkServerHealth", () => {
|
|||||||
}) as unknown as typeof globalThis.fetch
|
}) as unknown as typeof globalThis.fetch
|
||||||
|
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
await checkServerHealth("http://localhost:4096", fetch, { signal: abort.signal })
|
await checkServerHealth(server, fetch, {
|
||||||
|
signal: abort.signal,
|
||||||
|
})
|
||||||
|
|
||||||
expect(signal).toBe(abort.signal)
|
expect(signal).toBe(abort.signal)
|
||||||
})
|
})
|
||||||
@@ -87,7 +96,7 @@ describe("checkServerHealth", () => {
|
|||||||
})
|
})
|
||||||
}) as unknown as typeof globalThis.fetch
|
}) as unknown as typeof globalThis.fetch
|
||||||
|
|
||||||
const result = await checkServerHealth("http://localhost:4096", fetch, {
|
const result = await checkServerHealth(server, fetch, {
|
||||||
retryCount: 2,
|
retryCount: 2,
|
||||||
retryDelayMs: 1,
|
retryDelayMs: 1,
|
||||||
})
|
})
|
||||||
@@ -103,7 +112,7 @@ describe("checkServerHealth", () => {
|
|||||||
throw new TypeError("network")
|
throw new TypeError("network")
|
||||||
}) as unknown as typeof globalThis.fetch
|
}) as unknown as typeof globalThis.fetch
|
||||||
|
|
||||||
const result = await checkServerHealth("http://localhost:4096", fetch, {
|
const result = await checkServerHealth(server, fetch, {
|
||||||
retryCount: 2,
|
retryCount: 2,
|
||||||
retryDelayMs: 1,
|
retryDelayMs: 1,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import type { ServerConnection } from "@/context/server"
|
||||||
|
import { createSdkForServer } from "./server"
|
||||||
|
|
||||||
export type ServerHealth = { healthy: boolean; version?: string }
|
export type ServerHealth = { healthy: boolean; version?: string }
|
||||||
|
|
||||||
@@ -17,7 +18,10 @@ function timeoutSignal(timeoutMs: number) {
|
|||||||
const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout
|
const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
try {
|
try {
|
||||||
return { signal: timeout.call(AbortSignal, timeoutMs), clear: undefined as (() => void) | undefined }
|
return {
|
||||||
|
signal: timeout.call(AbortSignal, timeoutMs),
|
||||||
|
clear: undefined as (() => void) | undefined,
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
@@ -52,7 +56,7 @@ function retryable(error: unknown, signal?: AbortSignal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function checkServerHealth(
|
export async function checkServerHealth(
|
||||||
url: string,
|
server: ServerConnection.HttpBase,
|
||||||
fetch: typeof globalThis.fetch,
|
fetch: typeof globalThis.fetch,
|
||||||
opts?: CheckServerHealthOptions,
|
opts?: CheckServerHealthOptions,
|
||||||
): Promise<ServerHealth> {
|
): Promise<ServerHealth> {
|
||||||
@@ -67,8 +71,8 @@ export async function checkServerHealth(
|
|||||||
.catch(() => ({ healthy: false }))
|
.catch(() => ({ healthy: false }))
|
||||||
}
|
}
|
||||||
const attempt = (count: number): Promise<ServerHealth> =>
|
const attempt = (count: number): Promise<ServerHealth> =>
|
||||||
createOpencodeClient({
|
createSdkForServer({
|
||||||
baseUrl: url,
|
server,
|
||||||
fetch,
|
fetch,
|
||||||
signal,
|
signal,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import type { ServerConnection } from "@/context/server"
|
||||||
|
|
||||||
|
export function createSdkForServer({
|
||||||
|
server,
|
||||||
|
...config
|
||||||
|
}: Omit<NonNullable<Parameters<typeof createOpencodeClient>[0]>, "baseUrl"> & {
|
||||||
|
server: ServerConnection.HttpBase
|
||||||
|
}) {
|
||||||
|
const auth = (() => {
|
||||||
|
if (!server.password) return
|
||||||
|
return {
|
||||||
|
Authorization: `Basic ${btoa(`${server.username ?? "opencode"}:${server.password}`)}`,
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return createOpencodeClient({
|
||||||
|
...config,
|
||||||
|
headers: { ...config.headers, ...auth },
|
||||||
|
baseUrl: server.url,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,7 +6,10 @@ describe("terminalWriter", () => {
|
|||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const scheduled: VoidFunction[] = []
|
const scheduled: VoidFunction[] = []
|
||||||
const writer = terminalWriter(
|
const writer = terminalWriter(
|
||||||
(data) => calls.push(data),
|
(data, done) => {
|
||||||
|
calls.push(data)
|
||||||
|
done?.()
|
||||||
|
},
|
||||||
(flush) => scheduled.push(flush),
|
(flush) => scheduled.push(flush),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,10 +27,38 @@ describe("terminalWriter", () => {
|
|||||||
test("flush is a no-op when empty", () => {
|
test("flush is a no-op when empty", () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const writer = terminalWriter(
|
const writer = terminalWriter(
|
||||||
(data) => calls.push(data),
|
(data, done) => {
|
||||||
|
calls.push(data)
|
||||||
|
done?.()
|
||||||
|
},
|
||||||
(flush) => flush(),
|
(flush) => flush(),
|
||||||
)
|
)
|
||||||
writer.flush()
|
writer.flush()
|
||||||
expect(calls).toEqual([])
|
expect(calls).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("flush waits for pending write completion", () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
let done: VoidFunction | undefined
|
||||||
|
const writer = terminalWriter(
|
||||||
|
(data, finish) => {
|
||||||
|
calls.push(data)
|
||||||
|
done = finish
|
||||||
|
},
|
||||||
|
(flush) => flush(),
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.push("a")
|
||||||
|
|
||||||
|
let settled = false
|
||||||
|
writer.flush(() => {
|
||||||
|
settled = true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls).toEqual(["a"])
|
||||||
|
expect(settled).toBe(false)
|
||||||
|
|
||||||
|
done?.()
|
||||||
|
expect(settled).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,42 @@
|
|||||||
export function terminalWriter(
|
export function terminalWriter(
|
||||||
write: (data: string) => void,
|
write: (data: string, done?: VoidFunction) => void,
|
||||||
schedule: (flush: VoidFunction) => void = queueMicrotask,
|
schedule: (flush: VoidFunction) => void = queueMicrotask,
|
||||||
) {
|
) {
|
||||||
let chunks: string[] | undefined
|
let chunks: string[] | undefined
|
||||||
|
let waits: VoidFunction[] | undefined
|
||||||
let scheduled = false
|
let scheduled = false
|
||||||
|
let writing = false
|
||||||
|
|
||||||
const flush = () => {
|
const settle = () => {
|
||||||
|
if (scheduled || writing || chunks?.length) return
|
||||||
|
const list = waits
|
||||||
|
if (!list?.length) return
|
||||||
|
waits = undefined
|
||||||
|
for (const fn of list) {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
if (writing) return
|
||||||
scheduled = false
|
scheduled = false
|
||||||
const items = chunks
|
const items = chunks
|
||||||
if (!items?.length) return
|
if (!items?.length) {
|
||||||
|
settle()
|
||||||
|
return
|
||||||
|
}
|
||||||
chunks = undefined
|
chunks = undefined
|
||||||
write(items.join(""))
|
writing = true
|
||||||
|
write(items.join(""), () => {
|
||||||
|
writing = false
|
||||||
|
if (chunks?.length) {
|
||||||
|
if (scheduled) return
|
||||||
|
scheduled = true
|
||||||
|
schedule(run)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settle()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const push = (data: string) => {
|
const push = (data: string) => {
|
||||||
@@ -18,9 +44,21 @@ export function terminalWriter(
|
|||||||
if (chunks) chunks.push(data)
|
if (chunks) chunks.push(data)
|
||||||
else chunks = [data]
|
else chunks = [data]
|
||||||
|
|
||||||
if (scheduled) return
|
if (scheduled || writing) return
|
||||||
scheduled = true
|
scheduled = true
|
||||||
schedule(flush)
|
schedule(run)
|
||||||
|
}
|
||||||
|
|
||||||
|
const flush = (done?: VoidFunction) => {
|
||||||
|
if (!scheduled && !writing && !chunks?.length) {
|
||||||
|
done?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
if (waits) waits.push(done)
|
||||||
|
else waits = [done]
|
||||||
|
}
|
||||||
|
run()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { push, flush }
|
return { push, flush }
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
/// <reference path="../../sst-env.d.ts" />
|
/// <reference path="../../sst-env.d.ts" />
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "package.json"],
|
"include": ["src", "package.json"],
|
||||||
"exclude": ["dist", "ts-dist"]
|
"exclude": ["dist", "ts-dist"],
|
||||||
|
"references": [{ "path": "../sdk/js" }]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
/// <reference path="../../../sst-env.d.ts" />
|
/// <reference path="../../../sst-env.d.ts" />
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
import "sst"
|
import "sst"
|
||||||
declare module "sst" {
|
declare module "sst" {
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
import "sst"
|
import "sst"
|
||||||
declare module "sst" {
|
declare module "sst" {
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
/// <reference path="../../../sst-env.d.ts" />
|
/// <reference path="../../../sst-env.d.ts" />
|
||||||
|
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
import "sst"
|
import "sst"
|
||||||
declare module "sst" {
|
declare module "sst" {
|
||||||
|
|||||||
Generated
+1
@@ -3136,6 +3136,7 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ chrono = "0.4"
|
|||||||
tokio-stream = { version = "0.1.18", features = ["sync"] }
|
tokio-stream = { version = "0.1.18", features = ["sync"] }
|
||||||
process-wrap = { version = "9.0.3", features = ["tokio1"] }
|
process-wrap = { version = "9.0.3", features = ["tokio1"] }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows = { version = "0.62", features = ["Win32_System_Threading"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
gtk = "0.18.2"
|
gtk = "0.18.2"
|
||||||
webkit2gtk = "=2.0.2"
|
webkit2gtk = "=2.0.2"
|
||||||
|
|||||||
@@ -3,20 +3,39 @@ use process_wrap::tokio::CommandWrap;
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use process_wrap::tokio::ProcessGroup;
|
use process_wrap::tokio::ProcessGroup;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use process_wrap::tokio::{JobObject, KillOnDrop};
|
use process_wrap::tokio::{CommandWrapper, JobObject, KillOnDrop};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{process::Stdio, time::Duration};
|
use std::{process::Stdio, time::Duration};
|
||||||
use tauri::{AppHandle, Manager, path::BaseDirectory};
|
use tauri::{AppHandle, Manager, path::BaseDirectory};
|
||||||
use tauri_plugin_store::StoreExt;
|
|
||||||
use tauri_specta::Event;
|
use tauri_specta::Event;
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::{
|
||||||
use tokio::process::Command;
|
io::{AsyncBufRead, AsyncBufReadExt, BufReader},
|
||||||
use tokio::sync::{mpsc, oneshot};
|
process::Command,
|
||||||
|
sync::{mpsc, oneshot},
|
||||||
|
task::JoinHandle,
|
||||||
|
};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use windows::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED};
|
||||||
|
|
||||||
use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY};
|
use crate::server::get_wsl_config;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
// Keep this as a custom wrapper instead of process_wrap::CreationFlags.
|
||||||
|
// JobObject pre_spawn rewrites creation flags, so this must run after it.
|
||||||
|
struct WinCreationFlags;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
impl CommandWrapper for WinCreationFlags {
|
||||||
|
fn pre_spawn(&mut self, command: &mut Command, _core: &CommandWrap) -> std::io::Result<()> {
|
||||||
|
command.creation_flags((CREATE_NO_WINDOW | CREATE_SUSPENDED).0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CLI_INSTALL_DIR: &str = ".opencode/bin";
|
const CLI_INSTALL_DIR: &str = ".opencode/bin";
|
||||||
const CLI_BINARY_NAME: &str = "opencode";
|
const CLI_BINARY_NAME: &str = "opencode";
|
||||||
@@ -34,8 +53,8 @@ pub struct Config {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum CommandEvent {
|
pub enum CommandEvent {
|
||||||
Stdout(Vec<u8>),
|
Stdout(String),
|
||||||
Stderr(Vec<u8>),
|
Stderr(String),
|
||||||
Error(String),
|
Error(String),
|
||||||
Terminated(TerminatedPayload),
|
Terminated(TerminatedPayload),
|
||||||
}
|
}
|
||||||
@@ -64,10 +83,11 @@ pub async fn get_config(app: &AppHandle) -> Option<Config> {
|
|||||||
|
|
||||||
events
|
events
|
||||||
.fold(String::new(), async |mut config_str, event| {
|
.fold(String::new(), async |mut config_str, event| {
|
||||||
if let CommandEvent::Stdout(stdout) = event
|
if let CommandEvent::Stdout(s) = &event {
|
||||||
&& let Ok(s) = str::from_utf8(&stdout)
|
config_str += s.as_str()
|
||||||
{
|
}
|
||||||
config_str += s
|
if let CommandEvent::Stderr(s) = &event {
|
||||||
|
config_str += s.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
config_str
|
config_str
|
||||||
@@ -197,16 +217,8 @@ fn get_user_shell() -> String {
|
|||||||
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
|
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_wsl_enabled(app: &tauri::AppHandle) -> bool {
|
fn is_wsl_enabled(_app: &tauri::AppHandle) -> bool {
|
||||||
let Ok(store) = app.store(SETTINGS_STORE) else {
|
get_wsl_config(_app.clone()).is_ok_and(|v| v.enabled)
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
store
|
|
||||||
.get(WSL_ENABLED_KEY)
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|value| value.as_bool())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shell_escape(input: &str) -> String {
|
fn shell_escape(input: &str) -> String {
|
||||||
@@ -317,12 +329,9 @@ pub fn spawn_command(
|
|||||||
cmd
|
cmd
|
||||||
};
|
};
|
||||||
|
|
||||||
cmd.stdin(Stdio::null());
|
|
||||||
cmd.stdout(Stdio::piped());
|
cmd.stdout(Stdio::piped());
|
||||||
cmd.stderr(Stdio::piped());
|
cmd.stderr(Stdio::piped());
|
||||||
|
cmd.stdin(Stdio::null());
|
||||||
#[cfg(windows)]
|
|
||||||
cmd.creation_flags(0x0800_0000);
|
|
||||||
|
|
||||||
let mut wrap = CommandWrap::from(cmd);
|
let mut wrap = CommandWrap::from(cmd);
|
||||||
|
|
||||||
@@ -333,36 +342,28 @@ pub fn spawn_command(
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
wrap.wrap(JobObject).wrap(KillOnDrop);
|
wrap.wrap(JobObject).wrap(WinCreationFlags).wrap(KillOnDrop);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut child = wrap.spawn()?;
|
let mut child = wrap.spawn()?;
|
||||||
let stdout = child.stdout().take();
|
let guard = Arc::new(tokio::sync::RwLock::new(()));
|
||||||
let stderr = child.stderr().take();
|
|
||||||
let (tx, rx) = mpsc::channel(256);
|
let (tx, rx) = mpsc::channel(256);
|
||||||
let (kill_tx, mut kill_rx) = mpsc::channel(1);
|
let (kill_tx, mut kill_rx) = mpsc::channel(1);
|
||||||
|
|
||||||
if let Some(stdout) = stdout {
|
let stdout = spawn_pipe_reader(
|
||||||
let tx = tx.clone();
|
tx.clone(),
|
||||||
tokio::spawn(async move {
|
guard.clone(),
|
||||||
let mut lines = BufReader::new(stdout).lines();
|
BufReader::new(child.stdout().take().unwrap()),
|
||||||
while let Ok(Some(line)) = lines.next_line().await {
|
CommandEvent::Stdout,
|
||||||
let _ = tx.send(CommandEvent::Stdout(line.into_bytes())).await;
|
);
|
||||||
}
|
let stderr = spawn_pipe_reader(
|
||||||
});
|
tx.clone(),
|
||||||
}
|
guard.clone(),
|
||||||
|
BufReader::new(child.stderr().take().unwrap()),
|
||||||
|
CommandEvent::Stderr,
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(stderr) = stderr {
|
tokio::task::spawn(async move {
|
||||||
let tx = tx.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut lines = BufReader::new(stderr).lines();
|
|
||||||
while let Ok(Some(line)) = lines.next_line().await {
|
|
||||||
let _ = tx.send(CommandEvent::Stderr(line.into_bytes())).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut kill_open = true;
|
let mut kill_open = true;
|
||||||
let status = loop {
|
let status = loop {
|
||||||
match child.try_wait() {
|
match child.try_wait() {
|
||||||
@@ -394,6 +395,9 @@ pub fn spawn_command(
|
|||||||
let _ = tx.send(CommandEvent::Error(err.to_string())).await;
|
let _ = tx.send(CommandEvent::Error(err.to_string())).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stdout.abort();
|
||||||
|
stderr.abort();
|
||||||
});
|
});
|
||||||
|
|
||||||
let event_stream = ReceiverStream::new(rx);
|
let event_stream = ReceiverStream::new(rx);
|
||||||
@@ -404,9 +408,7 @@ pub fn spawn_command(
|
|||||||
|
|
||||||
fn signal_from_status(status: std::process::ExitStatus) -> Option<i32> {
|
fn signal_from_status(status: std::process::ExitStatus) -> Option<i32> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
return status.signal();
|
||||||
return status.signal();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
@@ -442,12 +444,10 @@ pub fn serve(
|
|||||||
events
|
events
|
||||||
.for_each(move |event| {
|
.for_each(move |event| {
|
||||||
match event {
|
match event {
|
||||||
CommandEvent::Stdout(line_bytes) => {
|
CommandEvent::Stdout(line) => {
|
||||||
let line = String::from_utf8_lossy(&line_bytes);
|
|
||||||
tracing::info!("{line}");
|
tracing::info!("{line}");
|
||||||
}
|
}
|
||||||
CommandEvent::Stderr(line_bytes) => {
|
CommandEvent::Stderr(line) => {
|
||||||
let line = String::from_utf8_lossy(&line_bytes);
|
|
||||||
tracing::info!("{line}");
|
tracing::info!("{line}");
|
||||||
}
|
}
|
||||||
CommandEvent::Error(err) => {
|
CommandEvent::Error(err) => {
|
||||||
@@ -499,11 +499,7 @@ pub mod sqlite_migration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
future::ready(match &event {
|
future::ready(match &event {
|
||||||
CommandEvent::Stdout(stdout) => {
|
CommandEvent::Stdout(s) | CommandEvent::Stderr(s) => {
|
||||||
let Ok(s) = str::from_utf8(stdout) else {
|
|
||||||
return future::ready(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(s) = s.strip_prefix("sqlite-migration:").map(|s| s.trim()) {
|
if let Some(s) = s.strip_prefix("sqlite-migration:").map(|s| s.trim()) {
|
||||||
if let Ok(progress) = s.parse::<u8>() {
|
if let Ok(progress) = s.parse::<u8>() {
|
||||||
let _ = SqliteMigrationProgress::InProgress(progress).emit(&app);
|
let _ = SqliteMigrationProgress::InProgress(progress).emit(&app);
|
||||||
@@ -522,3 +518,41 @@ pub mod sqlite_migration {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn_pipe_reader<F: Fn(String) -> CommandEvent + Send + Copy + 'static>(
|
||||||
|
tx: mpsc::Sender<CommandEvent>,
|
||||||
|
guard: Arc<tokio::sync::RwLock<()>>,
|
||||||
|
pipe_reader: impl AsyncBufRead + Send + Unpin + 'static,
|
||||||
|
wrapper: F,
|
||||||
|
) -> JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _lock = guard.read().await;
|
||||||
|
let reader = BufReader::new(pipe_reader);
|
||||||
|
|
||||||
|
read_line(reader, tx, wrapper).await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_line<F: Fn(String) -> CommandEvent + Send + Copy + 'static>(
|
||||||
|
reader: BufReader<impl AsyncBufRead + Unpin>,
|
||||||
|
tx: mpsc::Sender<CommandEvent>,
|
||||||
|
wrapper: F,
|
||||||
|
) {
|
||||||
|
let mut lines = reader.lines();
|
||||||
|
loop {
|
||||||
|
let line = lines.next_line().await;
|
||||||
|
|
||||||
|
match line {
|
||||||
|
Ok(s) => {
|
||||||
|
if let Some(s) = s {
|
||||||
|
let _ = tx.clone().send(wrapper(s)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let tx_ = tx.clone();
|
||||||
|
let _ = tx_.send(CommandEvent::Error(e.to_string())).await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,18 +55,18 @@ pub async fn set_default_server_url(app: AppHandle, url: Option<String>) -> Resu
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn get_wsl_config(app: AppHandle) -> Result<WslConfig, String> {
|
pub fn get_wsl_config(_app: AppHandle) -> Result<WslConfig, String> {
|
||||||
let store = app
|
// let store = app
|
||||||
.store(SETTINGS_STORE)
|
// .store(SETTINGS_STORE)
|
||||||
.map_err(|e| format!("Failed to open settings store: {}", e))?;
|
// .map_err(|e| format!("Failed to open settings store: {}", e))?;
|
||||||
|
|
||||||
let enabled = store
|
// let enabled = store
|
||||||
.get(WSL_ENABLED_KEY)
|
// .get(WSL_ENABLED_KEY)
|
||||||
.as_ref()
|
// .as_ref()
|
||||||
.and_then(|v| v.as_bool())
|
// .and_then(|v| v.as_bool())
|
||||||
.unwrap_or(false);
|
// .unwrap_or(false);
|
||||||
|
|
||||||
Ok(WslConfig { enabled })
|
Ok(WslConfig { enabled: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -1,36 +1,37 @@
|
|||||||
// @refresh reload
|
// @refresh reload
|
||||||
import { webviewZoom } from "./webview-zoom"
|
|
||||||
import { render } from "solid-js/web"
|
|
||||||
import {
|
import {
|
||||||
AppBaseProviders,
|
AppBaseProviders,
|
||||||
AppInterface,
|
AppInterface,
|
||||||
PlatformProvider,
|
|
||||||
Platform,
|
|
||||||
useCommand,
|
|
||||||
handleNotificationClick,
|
handleNotificationClick,
|
||||||
|
type Platform,
|
||||||
|
PlatformProvider,
|
||||||
|
ServerConnection,
|
||||||
|
useCommand,
|
||||||
} from "@opencode-ai/app"
|
} from "@opencode-ai/app"
|
||||||
import { open, save } from "@tauri-apps/plugin-dialog"
|
|
||||||
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
|
|
||||||
import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener"
|
|
||||||
import { open as shellOpen } from "@tauri-apps/plugin-shell"
|
|
||||||
import { type as ostype } from "@tauri-apps/plugin-os"
|
|
||||||
import { check, Update } from "@tauri-apps/plugin-updater"
|
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
|
||||||
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
|
|
||||||
import { relaunch } from "@tauri-apps/plugin-process"
|
|
||||||
import { AsyncStorage } from "@solid-primitives/storage"
|
|
||||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
|
|
||||||
import { Store } from "@tauri-apps/plugin-store"
|
|
||||||
import { Splash } from "@opencode-ai/ui/logo"
|
import { Splash } from "@opencode-ai/ui/logo"
|
||||||
import { createSignal, Show, Accessor, JSX, createResource, onMount, onCleanup } from "solid-js"
|
import type { AsyncStorage } from "@solid-primitives/storage"
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||||
import { readImage } from "@tauri-apps/plugin-clipboard-manager"
|
import { readImage } from "@tauri-apps/plugin-clipboard-manager"
|
||||||
|
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
|
||||||
import { UPDATER_ENABLED } from "./updater"
|
import { open, save } from "@tauri-apps/plugin-dialog"
|
||||||
import { initI18n, t } from "./i18n"
|
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
|
||||||
|
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
|
||||||
|
import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener"
|
||||||
|
import { type as ostype } from "@tauri-apps/plugin-os"
|
||||||
|
import { relaunch } from "@tauri-apps/plugin-process"
|
||||||
|
import { open as shellOpen } from "@tauri-apps/plugin-shell"
|
||||||
|
import { Store } from "@tauri-apps/plugin-store"
|
||||||
|
import { check, type Update } from "@tauri-apps/plugin-updater"
|
||||||
|
import { type Accessor, createResource, type JSX, onCleanup, onMount, Show } from "solid-js"
|
||||||
|
import { render } from "solid-js/web"
|
||||||
import pkg from "../package.json"
|
import pkg from "../package.json"
|
||||||
|
import { initI18n, t } from "./i18n"
|
||||||
|
import { UPDATER_ENABLED } from "./updater"
|
||||||
|
import { webviewZoom } from "./webview-zoom"
|
||||||
import "./styles.css"
|
import "./styles.css"
|
||||||
import { commands, InitStep } from "./bindings"
|
|
||||||
import { Channel } from "@tauri-apps/api/core"
|
import { Channel } from "@tauri-apps/api/core"
|
||||||
|
import { commands, type InitStep } from "./bindings"
|
||||||
import { createMenu } from "./menu"
|
import { createMenu } from "./menu"
|
||||||
|
|
||||||
const root = document.getElementById("root")
|
const root = document.getElementById("root")
|
||||||
@@ -58,7 +59,7 @@ const listenForDeepLinks = async () => {
|
|||||||
await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined)
|
await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createPlatform = (password: Accessor<string | null>): Platform => {
|
const createPlatform = (): Platform => {
|
||||||
const os = (() => {
|
const os = (() => {
|
||||||
const type = ostype()
|
const type = ostype()
|
||||||
if (type === "macos" || type === "windows" || type === "linux") return type
|
if (type === "macos" || type === "windows" || type === "linux") return type
|
||||||
@@ -344,22 +345,10 @@ const createPlatform = (password: Accessor<string | null>): Platform => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
fetch: (input, init) => {
|
fetch: (input, init) => {
|
||||||
const pw = password()
|
|
||||||
|
|
||||||
const addHeader = (headers: Headers, password: string) => {
|
|
||||||
headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input instanceof Request) {
|
if (input instanceof Request) {
|
||||||
if (pw) addHeader(input.headers, pw)
|
|
||||||
return tauriFetch(input)
|
return tauriFetch(input)
|
||||||
} else {
|
} else {
|
||||||
const headers = new Headers(init?.headers)
|
return tauriFetch(input, init)
|
||||||
if (pw) addHeader(headers, pw)
|
|
||||||
return tauriFetch(input, {
|
|
||||||
...(init as any),
|
|
||||||
headers: headers,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -417,7 +406,11 @@ const createPlatform = (password: Accessor<string | null>): Platform => {
|
|||||||
return new Promise<File | null>((resolve) => {
|
return new Promise<File | null>((resolve) => {
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
if (!blob) return resolve(null)
|
if (!blob) return resolve(null)
|
||||||
resolve(new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png" }))
|
resolve(
|
||||||
|
new File([blob], `pasted-image-${Date.now()}.png`, {
|
||||||
|
type: "image/png",
|
||||||
|
}),
|
||||||
|
)
|
||||||
}, "image/png")
|
}, "image/png")
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -431,9 +424,7 @@ createMenu((id) => {
|
|||||||
void listenForDeepLinks()
|
void listenForDeepLinks()
|
||||||
|
|
||||||
render(() => {
|
render(() => {
|
||||||
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
|
const platform = createPlatform()
|
||||||
|
|
||||||
const platform = createPlatform(() => serverPassword())
|
|
||||||
|
|
||||||
function handleClick(e: MouseEvent) {
|
function handleClick(e: MouseEvent) {
|
||||||
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
|
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
|
||||||
@@ -455,9 +446,16 @@ render(() => {
|
|||||||
<AppBaseProviders>
|
<AppBaseProviders>
|
||||||
<ServerGate>
|
<ServerGate>
|
||||||
{(data) => {
|
{(data) => {
|
||||||
setServerPassword(data().password)
|
const server: ServerConnection.Sidecar = {
|
||||||
window.__OPENCODE__ ??= {}
|
displayName: "Local Server",
|
||||||
window.__OPENCODE__.serverPassword = data().password ?? undefined
|
type: "sidecar",
|
||||||
|
variant: "base",
|
||||||
|
http: {
|
||||||
|
url: data().url,
|
||||||
|
username: "opencode",
|
||||||
|
password: data().password ?? undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
function Inner() {
|
function Inner() {
|
||||||
const cmd = useCommand()
|
const cmd = useCommand()
|
||||||
@@ -468,7 +466,7 @@ render(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppInterface defaultUrl={data().url} isSidecar>
|
<AppInterface defaultServer={ServerConnection.key(server)} servers={[server]}>
|
||||||
<Inner />
|
<Inner />
|
||||||
</AppInterface>
|
</AppInterface>
|
||||||
)
|
)
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
/// <reference path="../../sst-env.d.ts" />
|
/// <reference path="../../sst-env.d.ts" />
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
import "sst"
|
import "sst"
|
||||||
declare module "sst" {
|
declare module "sst" {
|
||||||
|
|||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* deno-fmt-ignore-file */
|
/* deno-fmt-ignore-file */
|
||||||
|
/* biome-ignore-all lint: auto-generated */
|
||||||
|
|
||||||
import "sst"
|
import "sst"
|
||||||
declare module "sst" {
|
declare module "sst" {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/babel__core": "7.20.5",
|
"@types/babel__core": "7.20.5",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@types/mime-types": "3.0.1",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
@@ -74,7 +75,7 @@
|
|||||||
"@ai-sdk/vercel": "1.0.33",
|
"@ai-sdk/vercel": "1.0.33",
|
||||||
"@ai-sdk/xai": "2.0.51",
|
"@ai-sdk/xai": "2.0.51",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@gitlab/gitlab-ai-provider": "3.5.1",
|
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
@@ -110,6 +111,7 @@
|
|||||||
"hono-openapi": "catalog:",
|
"hono-openapi": "catalog:",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.6",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user