mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 09:28:27 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60d099a996 |
@@ -1,5 +1,6 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Report an issue that should be fixed
|
description: Report an issue that should be fixed
|
||||||
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
name: 🚀 Feature Request
|
name: 🚀 Feature Request
|
||||||
description: Suggest an idea, feature, or enhancement
|
description: Suggest an idea, feature, or enhancement
|
||||||
|
labels: [discussion]
|
||||||
title: "[FEATURE]:"
|
title: "[FEATURE]:"
|
||||||
|
|
||||||
body:
|
body:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
name: Question
|
name: Question
|
||||||
description: Ask a question
|
description: Ask a question
|
||||||
|
labels: ["question"]
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
|
|||||||
@@ -11,6 +11,5 @@ MrMushrooooom
|
|||||||
nexxeln
|
nexxeln
|
||||||
R44VC0RP
|
R44VC0RP
|
||||||
rekram1-node
|
rekram1-node
|
||||||
|
RhysSullivan
|
||||||
thdxr
|
thdxr
|
||||||
simonklee
|
|
||||||
vimtor
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Vouched contributors for this project.
|
||||||
|
#
|
||||||
|
# See https://github.com/mitchellh/vouch for details.
|
||||||
|
#
|
||||||
|
# Syntax:
|
||||||
|
# - One handle per line (without @), sorted alphabetically.
|
||||||
|
# - Optional platform prefix: platform:username (e.g., github:user).
|
||||||
|
# - Denounce with minus prefix: -username or -platform:username.
|
||||||
|
# - Optional details after a space following the handle.
|
||||||
|
adamdotdevin
|
||||||
|
-agusbasari29 AI PR slop
|
||||||
|
ariane-emory
|
||||||
|
-atharvau AI review spamming literally every PR
|
||||||
|
-borealbytes
|
||||||
|
-danieljoshuanazareth
|
||||||
|
-danieljoshuanazareth
|
||||||
|
edemaine
|
||||||
|
-florianleibert
|
||||||
|
fwang
|
||||||
|
iamdavidhill
|
||||||
|
jayair
|
||||||
|
kitlangton
|
||||||
|
kommander
|
||||||
|
-opencode2026
|
||||||
|
-opencodeengineer bot that spams issues
|
||||||
|
r44vc0rp
|
||||||
|
rekram1-node
|
||||||
|
-ricardo-m-l
|
||||||
|
-robinmordasiewicz
|
||||||
|
shantur
|
||||||
|
simonklee
|
||||||
|
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||||
|
thdxr
|
||||||
|
-toastythebot
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
name: "Setup Bun"
|
name: "Setup Bun"
|
||||||
description: "Setup Bun with caching and install dependencies"
|
description: "Setup Bun with caching and install dependencies"
|
||||||
inputs:
|
|
||||||
install-flags:
|
|
||||||
description: "Additional flags to pass to 'bun install'"
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
@@ -51,8 +46,8 @@ runs:
|
|||||||
# e.g. ./patches/ for standard-openapi
|
# e.g. ./patches/ for standard-openapi
|
||||||
# https://github.com/oven-sh/bun/issues/28147
|
# https://github.com/oven-sh/bun/issues/28147
|
||||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||||
bun install --linker hoisted ${{ inputs.install-flags }}
|
bun install --linker hoisted
|
||||||
else
|
else
|
||||||
bun install ${{ inputs.install-flags }}
|
bun install
|
||||||
fi
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
name: daily-issues-recap
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run at 6 PM EST (23:00 UTC, or 22:00 UTC during daylight saving)
|
||||||
|
- cron: "0 23 * * *"
|
||||||
|
workflow_dispatch: # Allow manual trigger for testing
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
daily-recap:
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Install opencode
|
||||||
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
- name: Generate daily issues recap
|
||||||
|
id: recap
|
||||||
|
env:
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
OPENCODE_PERMISSION: |
|
||||||
|
{
|
||||||
|
"bash": {
|
||||||
|
"*": "deny",
|
||||||
|
"gh issue*": "allow",
|
||||||
|
"gh search*": "allow"
|
||||||
|
},
|
||||||
|
"webfetch": "deny",
|
||||||
|
"edit": "deny",
|
||||||
|
"write": "deny"
|
||||||
|
}
|
||||||
|
run: |
|
||||||
|
# Get today's date range
|
||||||
|
TODAY=$(date -u +%Y-%m-%d)
|
||||||
|
|
||||||
|
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily issues recap for the OpenCode repository.
|
||||||
|
|
||||||
|
TODAY'S DATE: ${TODAY}
|
||||||
|
|
||||||
|
STEP 1: Gather today's issues
|
||||||
|
Search for all OPEN issues created today (${TODAY}) using:
|
||||||
|
gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
|
||||||
|
|
||||||
|
IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these:
|
||||||
|
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
||||||
|
This recap is specifically for COMMUNITY (external) issues only.
|
||||||
|
|
||||||
|
STEP 2: Analyze and categorize
|
||||||
|
For each issue created today, categorize it:
|
||||||
|
|
||||||
|
**Severity Assessment:**
|
||||||
|
- CRITICAL: Crashes, data loss, security issues, blocks major functionality
|
||||||
|
- HIGH: Significant bugs affecting many users, important features broken
|
||||||
|
- MEDIUM: Bugs with workarounds, minor features broken
|
||||||
|
- LOW: Minor issues, cosmetic, nice-to-haves
|
||||||
|
|
||||||
|
**Activity Assessment:**
|
||||||
|
- Note issues with high comment counts or engagement
|
||||||
|
- Note issues from repeat reporters (check if author has filed before)
|
||||||
|
|
||||||
|
STEP 3: Cross-reference with existing issues
|
||||||
|
For issues that seem like feature requests or recurring bugs:
|
||||||
|
- Search for similar older issues to identify patterns
|
||||||
|
- Note if this is a frequently requested feature
|
||||||
|
- Identify any issues that are duplicates of long-standing requests
|
||||||
|
|
||||||
|
STEP 4: Generate the recap
|
||||||
|
Create a structured recap with these sections:
|
||||||
|
|
||||||
|
===DISCORD_START===
|
||||||
|
**Daily Issues Recap - ${TODAY}**
|
||||||
|
|
||||||
|
**Summary Stats**
|
||||||
|
- Total issues opened today: [count]
|
||||||
|
- By category: [bugs/features/questions]
|
||||||
|
|
||||||
|
**Critical/High Priority Issues**
|
||||||
|
[List any CRITICAL or HIGH severity issues with brief descriptions and issue numbers]
|
||||||
|
|
||||||
|
**Most Active/Discussed**
|
||||||
|
[Issues with significant engagement or from active community members]
|
||||||
|
|
||||||
|
**Trending Topics**
|
||||||
|
[Patterns noticed - e.g., 'Multiple reports about X', 'Continued interest in Y feature']
|
||||||
|
|
||||||
|
**Duplicates & Related**
|
||||||
|
[Issues that relate to existing open issues]
|
||||||
|
===DISCORD_END===
|
||||||
|
|
||||||
|
STEP 5: Format for Discord
|
||||||
|
Format the recap as a Discord-compatible message:
|
||||||
|
- Use Discord markdown (**, __, etc.)
|
||||||
|
- BE EXTREMELY CONCISE - this is an EOD summary, not a detailed report
|
||||||
|
- Use hyperlinked issue numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/issues/1234>)
|
||||||
|
- Group related issues on single lines where possible
|
||||||
|
- Add emoji sparingly for critical items only
|
||||||
|
- HARD LIMIT: Keep under 1800 characters total
|
||||||
|
- Skip sections that have nothing notable (e.g., if no critical issues, omit that section)
|
||||||
|
- Prioritize signal over completeness - only surface what matters
|
||||||
|
|
||||||
|
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/recap_raw.txt
|
||||||
|
|
||||||
|
# Extract only the Discord message between markers
|
||||||
|
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/recap_raw.txt | grep -v '===DISCORD' > /tmp/recap.txt
|
||||||
|
|
||||||
|
echo "recap_file=/tmp/recap.txt" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Post to Discord
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
|
||||||
|
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
|
||||||
|
cat /tmp/recap.txt
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read the recap
|
||||||
|
RECAP_RAW=$(cat /tmp/recap.txt)
|
||||||
|
RECAP_LENGTH=${#RECAP_RAW}
|
||||||
|
|
||||||
|
echo "Recap length: ${RECAP_LENGTH} chars"
|
||||||
|
|
||||||
|
# Function to post a message to Discord
|
||||||
|
post_to_discord() {
|
||||||
|
local msg="$1"
|
||||||
|
local content=$(echo "$msg" | jq -Rs '.')
|
||||||
|
curl -s -H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-d "{\"content\": ${content}}" \
|
||||||
|
"$DISCORD_WEBHOOK_URL"
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# If under limit, send as single message
|
||||||
|
if [ "$RECAP_LENGTH" -le 1950 ]; then
|
||||||
|
post_to_discord "$RECAP_RAW"
|
||||||
|
else
|
||||||
|
echo "Splitting into multiple messages..."
|
||||||
|
remaining="$RECAP_RAW"
|
||||||
|
while [ ${#remaining} -gt 0 ]; do
|
||||||
|
if [ ${#remaining} -le 1950 ]; then
|
||||||
|
post_to_discord "$remaining"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
chunk="${remaining:0:1900}"
|
||||||
|
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
|
||||||
|
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
|
||||||
|
chunk="${remaining:0:$last_newline}"
|
||||||
|
remaining="${remaining:$((last_newline+1))}"
|
||||||
|
else
|
||||||
|
chunk="${remaining:0:1900}"
|
||||||
|
remaining="${remaining:1900}"
|
||||||
|
fi
|
||||||
|
post_to_discord "$chunk"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Posted daily recap to Discord"
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
name: daily-pr-recap
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run at 5pm EST (22:00 UTC, or 21:00 UTC during daylight saving)
|
||||||
|
- cron: "0 22 * * *"
|
||||||
|
workflow_dispatch: # Allow manual trigger for testing
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr-recap:
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Install opencode
|
||||||
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
- name: Generate daily PR recap
|
||||||
|
id: recap
|
||||||
|
env:
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
OPENCODE_PERMISSION: |
|
||||||
|
{
|
||||||
|
"bash": {
|
||||||
|
"*": "deny",
|
||||||
|
"gh pr*": "allow",
|
||||||
|
"gh search*": "allow"
|
||||||
|
},
|
||||||
|
"webfetch": "deny",
|
||||||
|
"edit": "deny",
|
||||||
|
"write": "deny"
|
||||||
|
}
|
||||||
|
run: |
|
||||||
|
TODAY=$(date -u +%Y-%m-%d)
|
||||||
|
|
||||||
|
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the OpenCode repository.
|
||||||
|
|
||||||
|
TODAY'S DATE: ${TODAY}
|
||||||
|
|
||||||
|
STEP 1: Gather PR data
|
||||||
|
Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}):
|
||||||
|
|
||||||
|
# Open PRs created today
|
||||||
|
gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||||
|
|
||||||
|
# Open PRs with activity today (updated today)
|
||||||
|
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||||
|
|
||||||
|
IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these:
|
||||||
|
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
||||||
|
This recap is specifically for COMMUNITY (external) contributions only.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
STEP 2: For high-activity PRs, check comment counts
|
||||||
|
For promising PRs, run:
|
||||||
|
gh pr view [NUMBER] --repo ${{ github.repository }} --json comments --jq '[.comments[] | select(.author.login != \"copilot-pull-request-reviewer\" and .author.login != \"github-actions\")] | length'
|
||||||
|
|
||||||
|
IMPORTANT: When counting comments/activity, EXCLUDE these bot accounts:
|
||||||
|
- copilot-pull-request-reviewer
|
||||||
|
- github-actions
|
||||||
|
|
||||||
|
STEP 3: Identify what matters (ONLY from today's PRs)
|
||||||
|
|
||||||
|
**Bug Fixes From Today:**
|
||||||
|
- PRs with 'fix' or 'bug' in title created/updated today
|
||||||
|
- Small bug fixes (< 100 lines changed) that are easy to review
|
||||||
|
- Bug fixes from community contributors
|
||||||
|
|
||||||
|
**High Activity Today:**
|
||||||
|
- PRs with significant human comments today (excluding bots listed above)
|
||||||
|
- PRs with back-and-forth discussion today
|
||||||
|
|
||||||
|
**Quick Wins:**
|
||||||
|
- Small PRs (< 50 lines) that are approved or nearly approved
|
||||||
|
- PRs that just need a final review
|
||||||
|
|
||||||
|
STEP 4: Generate the recap
|
||||||
|
Create a structured recap:
|
||||||
|
|
||||||
|
===DISCORD_START===
|
||||||
|
**Daily PR Recap - ${TODAY}**
|
||||||
|
|
||||||
|
**New PRs Today**
|
||||||
|
[PRs opened today - group by type: bug fixes, features, etc.]
|
||||||
|
|
||||||
|
**Active PRs Today**
|
||||||
|
[PRs with activity/updates today - significant discussion]
|
||||||
|
|
||||||
|
**Quick Wins**
|
||||||
|
[Small PRs ready to merge]
|
||||||
|
===DISCORD_END===
|
||||||
|
|
||||||
|
STEP 5: Format for Discord
|
||||||
|
- Use Discord markdown (**, __, etc.)
|
||||||
|
- BE EXTREMELY CONCISE - surface what we might miss
|
||||||
|
- Use hyperlinked PR numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/pull/1234>)
|
||||||
|
- Include PR author: [#1234](<url>) (@author)
|
||||||
|
- For bug fixes, add brief description of what it fixes
|
||||||
|
- Show line count for quick wins: \"(+15/-3 lines)\"
|
||||||
|
- HARD LIMIT: Keep under 1800 characters total
|
||||||
|
- Skip empty sections
|
||||||
|
- Focus on PRs that need human eyes
|
||||||
|
|
||||||
|
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/pr_recap_raw.txt
|
||||||
|
|
||||||
|
# Extract only the Discord message between markers
|
||||||
|
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/pr_recap_raw.txt | grep -v '===DISCORD' > /tmp/pr_recap.txt
|
||||||
|
|
||||||
|
echo "recap_file=/tmp/pr_recap.txt" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Post to Discord
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
|
||||||
|
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
|
||||||
|
cat /tmp/pr_recap.txt
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read the recap
|
||||||
|
RECAP_RAW=$(cat /tmp/pr_recap.txt)
|
||||||
|
RECAP_LENGTH=${#RECAP_RAW}
|
||||||
|
|
||||||
|
echo "Recap length: ${RECAP_LENGTH} chars"
|
||||||
|
|
||||||
|
# Function to post a message to Discord
|
||||||
|
post_to_discord() {
|
||||||
|
local msg="$1"
|
||||||
|
local content=$(echo "$msg" | jq -Rs '.')
|
||||||
|
curl -s -H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-d "{\"content\": ${content}}" \
|
||||||
|
"$DISCORD_WEBHOOK_URL"
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# If under limit, send as single message
|
||||||
|
if [ "$RECAP_LENGTH" -le 1950 ]; then
|
||||||
|
post_to_discord "$RECAP_RAW"
|
||||||
|
else
|
||||||
|
echo "Splitting into multiple messages..."
|
||||||
|
remaining="$RECAP_RAW"
|
||||||
|
while [ ${#remaining} -gt 0 ]; do
|
||||||
|
if [ ${#remaining} -le 1950 ]; then
|
||||||
|
post_to_discord "$remaining"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
chunk="${remaining:0:1900}"
|
||||||
|
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
|
||||||
|
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
|
||||||
|
chunk="${remaining:0:$last_newline}"
|
||||||
|
remaining="${remaining:$((last_newline+1))}"
|
||||||
|
else
|
||||||
|
chunk="${remaining:0:1900}"
|
||||||
|
remaining="${remaining:1900}"
|
||||||
|
fi
|
||||||
|
post_to_discord "$chunk"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Posted daily PR recap to Discord"
|
||||||
@@ -36,11 +36,3 @@ jobs:
|
|||||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||||
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
||||||
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
|
|
||||||
INCIDENT_API_KEY: ${{ secrets.INCIDENT_API_KEY }}
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
|
||||||
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
|
|
||||||
SENTRY_RELEASE: web@${{ github.sha }}
|
|
||||||
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
|
|
||||||
VITE_SENTRY_RELEASE: web@${{ github.sha }}
|
|
||||||
|
|||||||
+197
-57
@@ -88,7 +88,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
|
./packages/opencode/script/build.ts
|
||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||||
@@ -209,6 +209,182 @@ jobs:
|
|||||||
packages/opencode/dist/opencode-windows-x64
|
packages/opencode/dist/opencode-windows-x64
|
||||||
packages/opencode/dist/opencode-windows-x64-baseline
|
packages/opencode/dist/opencode-windows-x64-baseline
|
||||||
|
|
||||||
|
build-tauri:
|
||||||
|
needs:
|
||||||
|
- build-cli
|
||||||
|
- version
|
||||||
|
continue-on-error: false
|
||||||
|
env:
|
||||||
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||||
|
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
settings:
|
||||||
|
- host: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
- host: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
||||||
|
- host: windows-2025
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
- host: blacksmith-4vcpu-windows-2025
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
- host: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
- host: blacksmith-8vcpu-ubuntu-2404-arm
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
runs-on: ${{ matrix.settings.host }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- uses: apple-actions/import-codesign-certs@v2
|
||||||
|
if: ${{ runner.os == 'macOS' }}
|
||||||
|
with:
|
||||||
|
keychain: build
|
||||||
|
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Verify Certificate
|
||||||
|
if: ${{ runner.os == 'macOS' }}
|
||||||
|
run: |
|
||||||
|
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||||
|
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||||
|
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||||
|
echo "Certificate imported."
|
||||||
|
|
||||||
|
- name: Setup Apple API Key
|
||||||
|
if: ${{ runner.os == 'macOS' }}
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Azure login
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: azure/login@v2
|
||||||
|
with:
|
||||||
|
client-id: ${{ env.AZURE_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
|
||||||
|
- name: Cache apt packages
|
||||||
|
if: contains(matrix.settings.host, 'ubuntu')
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/apt-cache
|
||||||
|
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-${{ hashFiles('.github/workflows/publish.yml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.settings.target }}-apt-
|
||||||
|
|
||||||
|
- name: install dependencies (ubuntu only)
|
||||||
|
if: contains(matrix.settings.host, 'ubuntu')
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
sudo chmod -R a+rw ~/apt-cache
|
||||||
|
|
||||||
|
- name: install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.settings.target }}
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: packages/desktop/src-tauri
|
||||||
|
shared-key: ${{ matrix.settings.target }}
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
cd packages/desktop
|
||||||
|
bun ./scripts/prepare.ts
|
||||||
|
env:
|
||||||
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
|
||||||
|
RUST_TARGET: ${{ matrix.settings.target }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
|
|
||||||
|
- name: Resolve tauri portable SHA
|
||||||
|
if: contains(matrix.settings.host, 'ubuntu')
|
||||||
|
run: echo "TAURI_PORTABLE_SHA=$(git ls-remote https://github.com/tauri-apps/tauri.git refs/heads/feat/truly-portable-appimage | cut -f1)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||||
|
- name: Install tauri-cli from portable appimage branch
|
||||||
|
uses: taiki-e/cache-cargo-install-action@v3
|
||||||
|
if: contains(matrix.settings.host, 'ubuntu')
|
||||||
|
with:
|
||||||
|
tool: tauri-cli
|
||||||
|
git: https://github.com/tauri-apps/tauri
|
||||||
|
# branch: feat/truly-portable-appimage
|
||||||
|
rev: ${{ env.TAURI_PORTABLE_SHA }}
|
||||||
|
|
||||||
|
- name: Show tauri-cli version
|
||||||
|
if: contains(matrix.settings.host, 'ubuntu')
|
||||||
|
run: cargo tauri --version
|
||||||
|
|
||||||
|
- name: Setup git committer
|
||||||
|
id: committer
|
||||||
|
uses: ./.github/actions/setup-git-committer
|
||||||
|
with:
|
||||||
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
|
- name: Build and upload artifacts
|
||||||
|
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||||
|
timeout-minutes: 60
|
||||||
|
with:
|
||||||
|
projectPath: packages/desktop
|
||||||
|
uploadWorkflowArtifacts: true
|
||||||
|
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||||
|
args: --target ${{ matrix.settings.target }} --config ${{ (github.ref_name == 'beta' && './src-tauri/tauri.beta.conf.json') || './src-tauri/tauri.prod.conf.json' }} --verbose
|
||||||
|
updaterJsonPreferNsis: true
|
||||||
|
releaseId: ${{ needs.version.outputs.release }}
|
||||||
|
tagName: ${{ needs.version.outputs.tag }}
|
||||||
|
releaseDraft: true
|
||||||
|
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||||
|
repo: ${{ (github.ref_name == 'beta' && 'opencode-beta') || '' }}
|
||||||
|
releaseCommitish: ${{ github.sha }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||||
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||||
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||||
|
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||||
|
|
||||||
|
- name: Verify signed Windows desktop artifacts
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$files = @(
|
||||||
|
"${{ github.workspace }}\packages\desktop\src-tauri\sidecars\opencode-cli-${{ matrix.settings.target }}.exe"
|
||||||
|
)
|
||||||
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\src-tauri\target\${{ matrix.settings.target }}\release\bundle\nsis\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
|
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$sig = Get-AuthenticodeSignature $file
|
||||||
|
if ($sig.Status -ne "Valid") {
|
||||||
|
throw "Invalid signature for ${file}: $($sig.Status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
build-electron:
|
build-electron:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
@@ -226,14 +402,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
settings:
|
settings:
|
||||||
- host: macos-26-intel
|
- host: macos-latest
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
platform_flag: --mac --x64
|
platform_flag: --mac --x64
|
||||||
bun_install_flags: --os=darwin --cpu=x64
|
- host: macos-latest
|
||||||
- host: macos-26
|
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
platform_flag: --mac --arm64
|
platform_flag: --mac --arm64
|
||||||
bun_install_flags: --os=darwin --cpu=arm64
|
|
||||||
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
||||||
- host: "windows-2025"
|
- host: "windows-2025"
|
||||||
target: aarch64-pc-windows-msvc
|
target: aarch64-pc-windows-msvc
|
||||||
@@ -263,8 +437,6 @@ jobs:
|
|||||||
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
with:
|
|
||||||
install-flags: ${{ matrix.settings.bun_install_flags }}
|
|
||||||
|
|
||||||
- name: Azure login
|
- name: Azure login
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
@@ -304,7 +476,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: bun ./scripts/prepare.ts
|
run: bun ./scripts/prepare.ts
|
||||||
working-directory: packages/desktop
|
working-directory: packages/desktop-electron
|
||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
@@ -315,21 +487,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: bun run build
|
||||||
working-directory: packages/desktop
|
working-directory: packages/desktop-electron
|
||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
|
||||||
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
|
|
||||||
SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
|
|
||||||
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
|
|
||||||
VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }}
|
|
||||||
VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
|
|
||||||
|
|
||||||
- name: Package and publish
|
- name: Package and publish
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release
|
||||||
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish always --config electron-builder.config.ts
|
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish always --config electron-builder.config.ts
|
||||||
working-directory: packages/desktop
|
working-directory: packages/desktop-electron
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
@@ -343,43 +508,19 @@ jobs:
|
|||||||
- name: Package (no publish)
|
- name: Package (no publish)
|
||||||
if: ${{ !needs.version.outputs.release }}
|
if: ${{ !needs.version.outputs.release }}
|
||||||
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
|
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
|
||||||
working-directory: packages/desktop
|
working-directory: packages/desktop-electron
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
|
||||||
- name: Create and upload macOS .app.tar.gz
|
|
||||||
if: runner.os == 'macOS' && needs.version.outputs.release
|
|
||||||
working-directory: packages/desktop/dist
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
|
||||||
run: |
|
|
||||||
if [[ "${{ matrix.settings.target }}" == "x86_64-apple-darwin" ]]; then
|
|
||||||
APP_DIR="mac"
|
|
||||||
OUT_NAME="opencode-desktop-mac-x64.app.tar.gz"
|
|
||||||
elif [[ "${{ matrix.settings.target }}" == "aarch64-apple-darwin" ]]; then
|
|
||||||
APP_DIR="mac-arm64"
|
|
||||||
OUT_NAME="opencode-desktop-mac-arm64.app.tar.gz"
|
|
||||||
else
|
|
||||||
echo "Unknown macOS target: ${{ matrix.settings.target }}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
APP_PATH=$(find "$APP_DIR" -maxdepth 1 -name "*.app" -type d | head -1)
|
|
||||||
if [ -z "$APP_PATH" ]; then
|
|
||||||
echo "No .app bundle found in $APP_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
tar -czf "$OUT_NAME" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")"
|
|
||||||
gh release upload "v${{ needs.version.outputs.version }}" "$OUT_NAME" --clobber --repo "${{ needs.version.outputs.repo }}"
|
|
||||||
|
|
||||||
- name: Verify signed Windows Electron artifacts
|
- name: Verify signed Windows Electron artifacts
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$files = @()
|
$files = @()
|
||||||
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*.exe" | Select-Object -ExpandProperty FullName
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
|
||||||
|
|
||||||
foreach ($file in $files | Select-Object -Unique) {
|
foreach ($file in $files | Select-Object -Unique) {
|
||||||
$sig = Get-AuthenticodeSignature $file
|
$sig = Get-AuthenticodeSignature $file
|
||||||
@@ -390,20 +531,21 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-desktop-${{ matrix.settings.target }}
|
name: opencode-electron-${{ matrix.settings.target }}
|
||||||
path: packages/desktop/dist/*
|
path: packages/desktop-electron/dist/*
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release
|
||||||
with:
|
with:
|
||||||
name: latest-yml-${{ matrix.settings.target }}
|
name: latest-yml-${{ matrix.settings.target }}
|
||||||
path: packages/desktop/dist/latest*.yml
|
path: packages/desktop-electron/dist/latest*.yml
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs:
|
needs:
|
||||||
- version
|
- version
|
||||||
- build-cli
|
- build-cli
|
||||||
- sign-cli-windows
|
- sign-cli-windows
|
||||||
|
- build-tauri
|
||||||
- build-electron
|
- build-electron
|
||||||
if: always() && !failure() && !cancelled()
|
if: always() && !failure() && !cancelled()
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
@@ -430,6 +572,13 @@ jobs:
|
|||||||
node-version: "24"
|
node-version: "24"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Setup git committer
|
||||||
|
id: committer
|
||||||
|
uses: ./.github/actions/setup-git-committer
|
||||||
|
with:
|
||||||
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
@@ -451,13 +600,6 @@ jobs:
|
|||||||
pattern: latest-yml-*
|
pattern: latest-yml-*
|
||||||
path: /tmp/latest-yml
|
path: /tmp/latest-yml
|
||||||
|
|
||||||
- name: Setup git committer
|
|
||||||
id: committer
|
|
||||||
uses: ./.github/actions/setup-git-committer
|
|
||||||
with:
|
|
||||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
|
||||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
|
||||||
|
|
||||||
- name: Cache apt packages (AUR)
|
- name: Cache apt packages (AUR)
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -486,5 +628,3 @@ jobs:
|
|||||||
GH_REPO: ${{ needs.version.outputs.repo }}
|
GH_REPO: ${{ needs.version.outputs.repo }}
|
||||||
NPM_CONFIG_PROVENANCE: false
|
NPM_CONFIG_PROVENANCE: false
|
||||||
LATEST_YML_DIR: /tmp/latest-yml
|
LATEST_YML_DIR: /tmp/latest-yml
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Check PR guidelines compliance
|
- name: Check PR guidelines compliance
|
||||||
env:
|
env:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
|
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
|
||||||
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
||||||
run: |
|
run: |
|
||||||
PR_BODY=$(jq -r .body pr_data.json)
|
PR_BODY=$(jq -r .body pr_data.json)
|
||||||
opencode run -m opencode/gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}'
|
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
|
||||||
|
|
||||||
<pr-number>
|
<pr-number>
|
||||||
${{ steps.pr-number.outputs.number }}
|
${{ steps.pr-number.outputs.number }}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
name: vouch-check-issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check if issue author is denounced
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const author = context.payload.issue.user.login;
|
||||||
|
const issueNumber = context.payload.issue.number;
|
||||||
|
|
||||||
|
// Skip bots
|
||||||
|
if (author.endsWith('[bot]')) {
|
||||||
|
core.info(`Skipping bot: ${author}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the VOUCHED.td file via API (no checkout needed)
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
const response = await github.rest.repos.getContent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
path: '.github/VOUCHED.td',
|
||||||
|
});
|
||||||
|
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
core.info('No .github/VOUCHED.td file found, skipping check.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the .td file for vouched and denounced users
|
||||||
|
const vouched = new Set();
|
||||||
|
const denounced = new Map();
|
||||||
|
for (const line of content.split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
|
||||||
|
const isDenounced = trimmed.startsWith('-');
|
||||||
|
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
|
||||||
|
if (!rest) continue;
|
||||||
|
|
||||||
|
const spaceIdx = rest.indexOf(' ');
|
||||||
|
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
||||||
|
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
||||||
|
|
||||||
|
// Handle platform:username or bare username
|
||||||
|
// Only match bare usernames or github: prefix (skip other platforms)
|
||||||
|
const colonIdx = handle.indexOf(':');
|
||||||
|
if (colonIdx !== -1) {
|
||||||
|
const platform = handle.slice(0, colonIdx).toLowerCase();
|
||||||
|
if (platform !== 'github') continue;
|
||||||
|
}
|
||||||
|
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
||||||
|
if (!username) continue;
|
||||||
|
|
||||||
|
if (isDenounced) {
|
||||||
|
denounced.set(username.toLowerCase(), reason);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
vouched.add(username.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the author is denounced
|
||||||
|
const reason = denounced.get(author.toLowerCase());
|
||||||
|
if (reason !== undefined) {
|
||||||
|
// Author is denounced — close the issue
|
||||||
|
const body = 'This issue has been automatically closed.';
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
state: 'closed',
|
||||||
|
state_reason: 'not_planned',
|
||||||
|
});
|
||||||
|
|
||||||
|
core.info(`Closed issue #${issueNumber} from denounced user ${author}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author is positively vouched — add label
|
||||||
|
if (!vouched.has(author.toLowerCase())) {
|
||||||
|
core.info(`User ${author} is not denounced or vouched. Allowing issue.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
labels: ['Vouched'],
|
||||||
|
});
|
||||||
|
|
||||||
|
core.info(`Added vouched label to issue #${issueNumber} from ${author}`);
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
name: vouch-check-pr
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check if PR author is denounced
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const author = context.payload.pull_request.user.login;
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
|
||||||
|
// Skip bots
|
||||||
|
if (author.endsWith('[bot]')) {
|
||||||
|
core.info(`Skipping bot: ${author}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the VOUCHED.td file via API (no checkout needed)
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
const response = await github.rest.repos.getContent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
path: '.github/VOUCHED.td',
|
||||||
|
});
|
||||||
|
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
core.info('No .github/VOUCHED.td file found, skipping check.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the .td file for vouched and denounced users
|
||||||
|
const vouched = new Set();
|
||||||
|
const denounced = new Map();
|
||||||
|
for (const line of content.split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
|
||||||
|
const isDenounced = trimmed.startsWith('-');
|
||||||
|
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
|
||||||
|
if (!rest) continue;
|
||||||
|
|
||||||
|
const spaceIdx = rest.indexOf(' ');
|
||||||
|
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
||||||
|
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
||||||
|
|
||||||
|
// Handle platform:username or bare username
|
||||||
|
// Only match bare usernames or github: prefix (skip other platforms)
|
||||||
|
const colonIdx = handle.indexOf(':');
|
||||||
|
if (colonIdx !== -1) {
|
||||||
|
const platform = handle.slice(0, colonIdx).toLowerCase();
|
||||||
|
if (platform !== 'github') continue;
|
||||||
|
}
|
||||||
|
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
||||||
|
if (!username) continue;
|
||||||
|
|
||||||
|
if (isDenounced) {
|
||||||
|
denounced.set(username.toLowerCase(), reason);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
vouched.add(username.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the author is denounced
|
||||||
|
const reason = denounced.get(author.toLowerCase());
|
||||||
|
if (reason !== undefined) {
|
||||||
|
// Author is denounced — close the PR
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
body: 'This pull request has been automatically closed.',
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.pulls.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: prNumber,
|
||||||
|
state: 'closed',
|
||||||
|
});
|
||||||
|
|
||||||
|
core.info(`Closed PR #${prNumber} from denounced user ${author}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author is positively vouched — add label
|
||||||
|
if (!vouched.has(author.toLowerCase())) {
|
||||||
|
core.info(`User ${author} is not denounced or vouched. Allowing PR.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
labels: ['Vouched'],
|
||||||
|
});
|
||||||
|
|
||||||
|
core.info(`Added vouched label to PR #${prNumber} from ${author}`);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: vouch-manage-by-issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: vouch-manage
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
manage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup git committer
|
||||||
|
id: committer
|
||||||
|
uses: ./.github/actions/setup-git-committer
|
||||||
|
with:
|
||||||
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
|
- uses: mitchellh/vouch/action/manage-by-issue@main
|
||||||
|
with:
|
||||||
|
issue-id: ${{ github.event.issue.number }}
|
||||||
|
comment-id: ${{ github.event.comment.id }}
|
||||||
|
roles: admin,maintain,write
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
@@ -0,0 +1,900 @@
|
|||||||
|
---
|
||||||
|
description: Translate content for a specified locale while preserving technical terms
|
||||||
|
mode: subagent
|
||||||
|
model: opencode/gpt-5.4
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a professional translator and localization specialist.
|
||||||
|
|
||||||
|
Translate the user's content into the requested target locale (language + region, e.g. fr-FR, de-DE).
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
||||||
|
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
||||||
|
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
||||||
|
- Also apply locale-specific guidance from `.opencode/glossary/<locale>.md` when available (for example, `zh-cn.md`).
|
||||||
|
- Do not modify fenced code blocks.
|
||||||
|
- Output ONLY the translation (no commentary).
|
||||||
|
|
||||||
|
If the target locale is missing, ask the user to provide it.
|
||||||
|
If no locale-specific glossary exists, use the global glossary only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Locale-Specific Glossaries
|
||||||
|
|
||||||
|
When a locale glossary exists, use it to:
|
||||||
|
|
||||||
|
- Apply preferred wording for recurring UI/docs terms in that locale
|
||||||
|
- Preserve locale-specific do-not-translate terms and casing decisions
|
||||||
|
- Prefer natural phrasing over literal translation when the locale file calls it out
|
||||||
|
- If the repo uses a locale alias slug, apply that file too (for example, `pt-BR` maps to `br.md` in this repo)
|
||||||
|
|
||||||
|
Locale guidance does not override code/command preservation rules or the global Do-Not-Translate glossary below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Do-Not-Translate Terms (OpenCode Docs)
|
||||||
|
|
||||||
|
Generated from: `packages/web/src/content/docs/*.mdx` (default English docs)
|
||||||
|
Generated on: 2026-02-10
|
||||||
|
|
||||||
|
Use this as a translation QA checklist / glossary. Preserve listed terms exactly (spelling, casing, punctuation).
|
||||||
|
|
||||||
|
General rules (verbatim, even if not listed below):
|
||||||
|
|
||||||
|
- Anything inside inline code (single backticks) or fenced code blocks (triple backticks)
|
||||||
|
- MDX/JS code in docs: `import ... from "..."`, component tags, identifiers
|
||||||
|
- CLI commands, flags, config keys/values, file paths, URLs/domains, and env vars
|
||||||
|
|
||||||
|
## Proper nouns and product names
|
||||||
|
|
||||||
|
Additional (not reliably captured via link text):
|
||||||
|
|
||||||
|
```text
|
||||||
|
Astro
|
||||||
|
Bun
|
||||||
|
Chocolatey
|
||||||
|
Cursor
|
||||||
|
Docker
|
||||||
|
Git
|
||||||
|
GitHub Actions
|
||||||
|
GitLab CI
|
||||||
|
GNOME Terminal
|
||||||
|
Homebrew
|
||||||
|
Mise
|
||||||
|
Neovim
|
||||||
|
Node.js
|
||||||
|
npm
|
||||||
|
Obsidian
|
||||||
|
opencode
|
||||||
|
opencode-ai
|
||||||
|
Paru
|
||||||
|
pnpm
|
||||||
|
ripgrep
|
||||||
|
Scoop
|
||||||
|
SST
|
||||||
|
Starlight
|
||||||
|
Visual Studio Code
|
||||||
|
VS Code
|
||||||
|
VSCodium
|
||||||
|
Windsurf
|
||||||
|
Windows Terminal
|
||||||
|
Yarn
|
||||||
|
Zellij
|
||||||
|
Zed
|
||||||
|
anomalyco
|
||||||
|
```
|
||||||
|
|
||||||
|
Extracted from link labels in the English docs (review and prune as desired):
|
||||||
|
|
||||||
|
```text
|
||||||
|
@openspoon/subtask2
|
||||||
|
302.AI console
|
||||||
|
ACP progress report
|
||||||
|
Agent Client Protocol
|
||||||
|
Agent Skills
|
||||||
|
Agentic
|
||||||
|
AGENTS.md
|
||||||
|
AI SDK
|
||||||
|
Alacritty
|
||||||
|
Anthropic
|
||||||
|
Anthropic's Data Policies
|
||||||
|
Atom One
|
||||||
|
Avante.nvim
|
||||||
|
Ayu
|
||||||
|
Azure AI Foundry
|
||||||
|
Azure portal
|
||||||
|
Baseten
|
||||||
|
built-in GITHUB_TOKEN
|
||||||
|
Bun.$
|
||||||
|
Catppuccin
|
||||||
|
Cerebras console
|
||||||
|
ChatGPT Plus or Pro
|
||||||
|
Cloudflare dashboard
|
||||||
|
CodeCompanion.nvim
|
||||||
|
CodeNomad
|
||||||
|
Configuring Adapters: Environment Variables
|
||||||
|
Context7 MCP server
|
||||||
|
Cortecs console
|
||||||
|
Deep Infra dashboard
|
||||||
|
DeepSeek console
|
||||||
|
Duo Agent Platform
|
||||||
|
Everforest
|
||||||
|
Fireworks AI console
|
||||||
|
Firmware dashboard
|
||||||
|
Ghostty
|
||||||
|
GitLab CLI agents docs
|
||||||
|
GitLab docs
|
||||||
|
GitLab User Settings > Access Tokens
|
||||||
|
Granular Rules (Object Syntax)
|
||||||
|
Grep by Vercel
|
||||||
|
Groq console
|
||||||
|
Gruvbox
|
||||||
|
Helicone
|
||||||
|
Helicone documentation
|
||||||
|
Helicone Header Directory
|
||||||
|
Helicone's Model Directory
|
||||||
|
Hugging Face Inference Providers
|
||||||
|
Hugging Face settings
|
||||||
|
install WSL
|
||||||
|
IO.NET console
|
||||||
|
JetBrains IDE
|
||||||
|
Kanagawa
|
||||||
|
Kitty
|
||||||
|
MiniMax API Console
|
||||||
|
Models.dev
|
||||||
|
Moonshot AI console
|
||||||
|
Nebius Token Factory console
|
||||||
|
Nord
|
||||||
|
OAuth
|
||||||
|
Ollama integration docs
|
||||||
|
OpenAI's Data Policies
|
||||||
|
OpenChamber
|
||||||
|
OpenCode
|
||||||
|
OpenCode config
|
||||||
|
OpenCode Config
|
||||||
|
OpenCode TUI with the opencode theme
|
||||||
|
OpenCode Web - Active Session
|
||||||
|
OpenCode Web - New Session
|
||||||
|
OpenCode Web - See Servers
|
||||||
|
OpenCode Zen
|
||||||
|
OpenCode-Obsidian
|
||||||
|
OpenRouter dashboard
|
||||||
|
OpenWork
|
||||||
|
OVHcloud panel
|
||||||
|
Pro+ subscription
|
||||||
|
SAP BTP Cockpit
|
||||||
|
Scaleway Console IAM settings
|
||||||
|
Scaleway Generative APIs
|
||||||
|
SDK documentation
|
||||||
|
Sentry MCP server
|
||||||
|
shell API
|
||||||
|
Together AI console
|
||||||
|
Tokyonight
|
||||||
|
Unified Billing
|
||||||
|
Venice AI console
|
||||||
|
Vercel dashboard
|
||||||
|
WezTerm
|
||||||
|
Windows Subsystem for Linux (WSL)
|
||||||
|
WSL
|
||||||
|
WSL (Windows Subsystem for Linux)
|
||||||
|
WSL extension
|
||||||
|
xAI console
|
||||||
|
Z.AI API console
|
||||||
|
Zed
|
||||||
|
ZenMux dashboard
|
||||||
|
Zod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acronyms and initialisms
|
||||||
|
|
||||||
|
```text
|
||||||
|
ACP
|
||||||
|
AGENTS
|
||||||
|
AI
|
||||||
|
AI21
|
||||||
|
ANSI
|
||||||
|
API
|
||||||
|
AST
|
||||||
|
AWS
|
||||||
|
BTP
|
||||||
|
CD
|
||||||
|
CDN
|
||||||
|
CI
|
||||||
|
CLI
|
||||||
|
CMD
|
||||||
|
CORS
|
||||||
|
DEBUG
|
||||||
|
EKS
|
||||||
|
ERROR
|
||||||
|
FAQ
|
||||||
|
GLM
|
||||||
|
GNOME
|
||||||
|
GPT
|
||||||
|
HTML
|
||||||
|
HTTP
|
||||||
|
HTTPS
|
||||||
|
IAM
|
||||||
|
ID
|
||||||
|
IDE
|
||||||
|
INFO
|
||||||
|
IO
|
||||||
|
IP
|
||||||
|
IRSA
|
||||||
|
JS
|
||||||
|
JSON
|
||||||
|
JSONC
|
||||||
|
K2
|
||||||
|
LLM
|
||||||
|
LM
|
||||||
|
LSP
|
||||||
|
M2
|
||||||
|
MCP
|
||||||
|
MR
|
||||||
|
NET
|
||||||
|
NPM
|
||||||
|
NTLM
|
||||||
|
OIDC
|
||||||
|
OS
|
||||||
|
PAT
|
||||||
|
PATH
|
||||||
|
PHP
|
||||||
|
PR
|
||||||
|
PTY
|
||||||
|
README
|
||||||
|
RFC
|
||||||
|
RPC
|
||||||
|
SAP
|
||||||
|
SDK
|
||||||
|
SKILL
|
||||||
|
SSE
|
||||||
|
SSO
|
||||||
|
TS
|
||||||
|
TTY
|
||||||
|
TUI
|
||||||
|
UI
|
||||||
|
URL
|
||||||
|
US
|
||||||
|
UX
|
||||||
|
VCS
|
||||||
|
VPC
|
||||||
|
VPN
|
||||||
|
VS
|
||||||
|
WARN
|
||||||
|
WSL
|
||||||
|
X11
|
||||||
|
YAML
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code identifiers used in prose (CamelCase, mixedCase)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apiKey
|
||||||
|
AppleScript
|
||||||
|
AssistantMessage
|
||||||
|
baseURL
|
||||||
|
BurntSushi
|
||||||
|
ChatGPT
|
||||||
|
ClangFormat
|
||||||
|
CodeCompanion
|
||||||
|
CodeNomad
|
||||||
|
DeepSeek
|
||||||
|
DefaultV2
|
||||||
|
FileContent
|
||||||
|
FileDiff
|
||||||
|
FileNode
|
||||||
|
fineGrained
|
||||||
|
FormatterStatus
|
||||||
|
GitHub
|
||||||
|
GitLab
|
||||||
|
iTerm2
|
||||||
|
JavaScript
|
||||||
|
JetBrains
|
||||||
|
macOS
|
||||||
|
mDNS
|
||||||
|
MiniMax
|
||||||
|
NeuralNomadsAI
|
||||||
|
NickvanDyke
|
||||||
|
NoeFabris
|
||||||
|
OpenAI
|
||||||
|
OpenAPI
|
||||||
|
OpenChamber
|
||||||
|
OpenCode
|
||||||
|
OpenRouter
|
||||||
|
OpenTUI
|
||||||
|
OpenWork
|
||||||
|
ownUserPermissions
|
||||||
|
PowerShell
|
||||||
|
ProviderAuthAuthorization
|
||||||
|
ProviderAuthMethod
|
||||||
|
ProviderInitError
|
||||||
|
SessionStatus
|
||||||
|
TabItem
|
||||||
|
tokenType
|
||||||
|
ToolIDs
|
||||||
|
ToolList
|
||||||
|
TypeScript
|
||||||
|
typesUrl
|
||||||
|
UserMessage
|
||||||
|
VcsInfo
|
||||||
|
WebView2
|
||||||
|
WezTerm
|
||||||
|
xAI
|
||||||
|
ZenMux
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenCode CLI commands (as shown in docs)
|
||||||
|
|
||||||
|
```text
|
||||||
|
opencode
|
||||||
|
opencode [project]
|
||||||
|
opencode /path/to/project
|
||||||
|
opencode acp
|
||||||
|
opencode agent [command]
|
||||||
|
opencode agent create
|
||||||
|
opencode agent list
|
||||||
|
opencode attach [url]
|
||||||
|
opencode attach http://10.20.30.40:4096
|
||||||
|
opencode attach http://localhost:4096
|
||||||
|
opencode auth [command]
|
||||||
|
opencode auth list
|
||||||
|
opencode auth login
|
||||||
|
opencode auth logout
|
||||||
|
opencode auth ls
|
||||||
|
opencode export [sessionID]
|
||||||
|
opencode github [command]
|
||||||
|
opencode github install
|
||||||
|
opencode github run
|
||||||
|
opencode import <file>
|
||||||
|
opencode import https://opncd.ai/s/abc123
|
||||||
|
opencode import session.json
|
||||||
|
opencode mcp [command]
|
||||||
|
opencode mcp add
|
||||||
|
opencode mcp auth [name]
|
||||||
|
opencode mcp auth list
|
||||||
|
opencode mcp auth ls
|
||||||
|
opencode mcp auth my-oauth-server
|
||||||
|
opencode mcp auth sentry
|
||||||
|
opencode mcp debug <name>
|
||||||
|
opencode mcp debug my-oauth-server
|
||||||
|
opencode mcp list
|
||||||
|
opencode mcp logout [name]
|
||||||
|
opencode mcp logout my-oauth-server
|
||||||
|
opencode mcp ls
|
||||||
|
opencode models --refresh
|
||||||
|
opencode models [provider]
|
||||||
|
opencode models anthropic
|
||||||
|
opencode run [message..]
|
||||||
|
opencode run Explain the use of context in Go
|
||||||
|
opencode serve
|
||||||
|
opencode serve --cors http://localhost:5173 --cors https://app.example.com
|
||||||
|
opencode serve --hostname 0.0.0.0 --port 4096
|
||||||
|
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
|
||||||
|
opencode session [command]
|
||||||
|
opencode session list
|
||||||
|
opencode session delete <sessionID>
|
||||||
|
opencode stats
|
||||||
|
opencode uninstall
|
||||||
|
opencode upgrade
|
||||||
|
opencode upgrade [target]
|
||||||
|
opencode upgrade v0.1.48
|
||||||
|
opencode web
|
||||||
|
opencode web --cors https://example.com
|
||||||
|
opencode web --hostname 0.0.0.0
|
||||||
|
opencode web --mdns
|
||||||
|
opencode web --mdns --mdns-domain myproject.local
|
||||||
|
opencode web --port 4096
|
||||||
|
opencode web --port 4096 --hostname 0.0.0.0
|
||||||
|
opencode.server.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Slash commands and routes
|
||||||
|
|
||||||
|
```text
|
||||||
|
/agent
|
||||||
|
/auth/:id
|
||||||
|
/clear
|
||||||
|
/command
|
||||||
|
/config
|
||||||
|
/config/providers
|
||||||
|
/connect
|
||||||
|
/continue
|
||||||
|
/doc
|
||||||
|
/editor
|
||||||
|
/event
|
||||||
|
/experimental/tool?provider=<p>&model=<m>
|
||||||
|
/experimental/tool/ids
|
||||||
|
/export
|
||||||
|
/file?path=<path>
|
||||||
|
/file/content?path=<p>
|
||||||
|
/file/status
|
||||||
|
/find?pattern=<pat>
|
||||||
|
/find/file
|
||||||
|
/find/file?query=<q>
|
||||||
|
/find/symbol?query=<q>
|
||||||
|
/formatter
|
||||||
|
/global/event
|
||||||
|
/global/health
|
||||||
|
/help
|
||||||
|
/init
|
||||||
|
/instance/dispose
|
||||||
|
/log
|
||||||
|
/lsp
|
||||||
|
/mcp
|
||||||
|
/mnt/
|
||||||
|
/mnt/c/
|
||||||
|
/mnt/d/
|
||||||
|
/models
|
||||||
|
/oc
|
||||||
|
/opencode
|
||||||
|
/path
|
||||||
|
/project
|
||||||
|
/project/current
|
||||||
|
/provider
|
||||||
|
/provider/{id}/oauth/authorize
|
||||||
|
/provider/{id}/oauth/callback
|
||||||
|
/provider/auth
|
||||||
|
/q
|
||||||
|
/quit
|
||||||
|
/redo
|
||||||
|
/resume
|
||||||
|
/session
|
||||||
|
/session/:id
|
||||||
|
/session/:id/abort
|
||||||
|
/session/:id/children
|
||||||
|
/session/:id/command
|
||||||
|
/session/:id/diff
|
||||||
|
/session/:id/fork
|
||||||
|
/session/:id/init
|
||||||
|
/session/:id/message
|
||||||
|
/session/:id/message/:messageID
|
||||||
|
/session/:id/permissions/:permissionID
|
||||||
|
/session/:id/prompt_async
|
||||||
|
/session/:id/revert
|
||||||
|
/session/:id/share
|
||||||
|
/session/:id/shell
|
||||||
|
/session/:id/summarize
|
||||||
|
/session/:id/todo
|
||||||
|
/session/:id/unrevert
|
||||||
|
/session/status
|
||||||
|
/share
|
||||||
|
/summarize
|
||||||
|
/theme
|
||||||
|
/tui
|
||||||
|
/tui/append-prompt
|
||||||
|
/tui/clear-prompt
|
||||||
|
/tui/control/next
|
||||||
|
/tui/control/response
|
||||||
|
/tui/execute-command
|
||||||
|
/tui/open-help
|
||||||
|
/tui/open-models
|
||||||
|
/tui/open-sessions
|
||||||
|
/tui/open-themes
|
||||||
|
/tui/show-toast
|
||||||
|
/tui/submit-prompt
|
||||||
|
/undo
|
||||||
|
/Users/username
|
||||||
|
/Users/username/projects/*
|
||||||
|
/vcs
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI flags and short options
|
||||||
|
|
||||||
|
```text
|
||||||
|
--agent
|
||||||
|
--attach
|
||||||
|
--command
|
||||||
|
--continue
|
||||||
|
--cors
|
||||||
|
--cwd
|
||||||
|
--days
|
||||||
|
--dir
|
||||||
|
--dry-run
|
||||||
|
--event
|
||||||
|
--file
|
||||||
|
--force
|
||||||
|
--fork
|
||||||
|
--format
|
||||||
|
--help
|
||||||
|
--hostname
|
||||||
|
--hostname 0.0.0.0
|
||||||
|
--keep-config
|
||||||
|
--keep-data
|
||||||
|
--log-level
|
||||||
|
--max-count
|
||||||
|
--mdns
|
||||||
|
--mdns-domain
|
||||||
|
--method
|
||||||
|
--model
|
||||||
|
--models
|
||||||
|
--port
|
||||||
|
--print-logs
|
||||||
|
--project
|
||||||
|
--prompt
|
||||||
|
--refresh
|
||||||
|
--session
|
||||||
|
--share
|
||||||
|
--title
|
||||||
|
--token
|
||||||
|
--tools
|
||||||
|
--verbose
|
||||||
|
--version
|
||||||
|
--wait
|
||||||
|
|
||||||
|
-c
|
||||||
|
-d
|
||||||
|
-f
|
||||||
|
-h
|
||||||
|
-m
|
||||||
|
-n
|
||||||
|
-s
|
||||||
|
-v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
```text
|
||||||
|
AI_API_URL
|
||||||
|
AI_FLOW_CONTEXT
|
||||||
|
AI_FLOW_EVENT
|
||||||
|
AI_FLOW_INPUT
|
||||||
|
AICORE_DEPLOYMENT_ID
|
||||||
|
AICORE_RESOURCE_GROUP
|
||||||
|
AICORE_SERVICE_KEY
|
||||||
|
ANTHROPIC_API_KEY
|
||||||
|
AWS_ACCESS_KEY_ID
|
||||||
|
AWS_BEARER_TOKEN_BEDROCK
|
||||||
|
AWS_PROFILE
|
||||||
|
AWS_REGION
|
||||||
|
AWS_ROLE_ARN
|
||||||
|
AWS_SECRET_ACCESS_KEY
|
||||||
|
AWS_WEB_IDENTITY_TOKEN_FILE
|
||||||
|
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||||
|
AZURE_RESOURCE_NAME
|
||||||
|
CI_PROJECT_DIR
|
||||||
|
CI_SERVER_FQDN
|
||||||
|
CI_WORKLOAD_REF
|
||||||
|
CLOUDFLARE_ACCOUNT_ID
|
||||||
|
CLOUDFLARE_API_TOKEN
|
||||||
|
CLOUDFLARE_GATEWAY_ID
|
||||||
|
CONTEXT7_API_KEY
|
||||||
|
GITHUB_TOKEN
|
||||||
|
GITLAB_AI_GATEWAY_URL
|
||||||
|
GITLAB_HOST
|
||||||
|
GITLAB_INSTANCE_URL
|
||||||
|
GITLAB_OAUTH_CLIENT_ID
|
||||||
|
GITLAB_TOKEN
|
||||||
|
GITLAB_TOKEN_OPENCODE
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS
|
||||||
|
GOOGLE_CLOUD_PROJECT
|
||||||
|
HTTP_PROXY
|
||||||
|
HTTPS_PROXY
|
||||||
|
K2_
|
||||||
|
MY_API_KEY
|
||||||
|
MY_ENV_VAR
|
||||||
|
MY_MCP_CLIENT_ID
|
||||||
|
MY_MCP_CLIENT_SECRET
|
||||||
|
NO_PROXY
|
||||||
|
NODE_ENV
|
||||||
|
NODE_EXTRA_CA_CERTS
|
||||||
|
NPM_AUTH_TOKEN
|
||||||
|
OC_ALLOW_WAYLAND
|
||||||
|
OPENCODE_API_KEY
|
||||||
|
OPENCODE_AUTH_JSON
|
||||||
|
OPENCODE_AUTO_SHARE
|
||||||
|
OPENCODE_CLIENT
|
||||||
|
OPENCODE_CONFIG
|
||||||
|
OPENCODE_CONFIG_CONTENT
|
||||||
|
OPENCODE_CONFIG_DIR
|
||||||
|
OPENCODE_DISABLE_AUTOCOMPACT
|
||||||
|
OPENCODE_DISABLE_AUTOUPDATE
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
||||||
|
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||||
|
OPENCODE_DISABLE_FILETIME_CHECK
|
||||||
|
OPENCODE_DISABLE_LSP_DOWNLOAD
|
||||||
|
OPENCODE_DISABLE_MODELS_FETCH
|
||||||
|
OPENCODE_DISABLE_PRUNE
|
||||||
|
OPENCODE_DISABLE_TERMINAL_TITLE
|
||||||
|
OPENCODE_ENABLE_EXA
|
||||||
|
OPENCODE_ENABLE_EXPERIMENTAL_MODELS
|
||||||
|
OPENCODE_EXPERIMENTAL
|
||||||
|
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS
|
||||||
|
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
|
||||||
|
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER
|
||||||
|
OPENCODE_EXPERIMENTAL_EXA
|
||||||
|
OPENCODE_EXPERIMENTAL_FILEWATCHER
|
||||||
|
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY
|
||||||
|
OPENCODE_EXPERIMENTAL_LSP_TOOL
|
||||||
|
OPENCODE_EXPERIMENTAL_LSP_TY
|
||||||
|
OPENCODE_EXPERIMENTAL_MARKDOWN
|
||||||
|
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX
|
||||||
|
OPENCODE_EXPERIMENTAL_OXFMT
|
||||||
|
OPENCODE_EXPERIMENTAL_PLAN_MODE
|
||||||
|
OPENCODE_ENABLE_QUESTION_TOOL
|
||||||
|
OPENCODE_FAKE_VCS
|
||||||
|
OPENCODE_GIT_BASH_PATH
|
||||||
|
OPENCODE_MODEL
|
||||||
|
OPENCODE_MODELS_URL
|
||||||
|
OPENCODE_PERMISSION
|
||||||
|
OPENCODE_PORT
|
||||||
|
OPENCODE_SERVER_PASSWORD
|
||||||
|
OPENCODE_SERVER_USERNAME
|
||||||
|
PROJECT_ROOT
|
||||||
|
RESOURCE_NAME
|
||||||
|
RUST_LOG
|
||||||
|
VARIABLE_NAME
|
||||||
|
VERTEX_LOCATION
|
||||||
|
XDG_CONFIG_HOME
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package/module identifiers
|
||||||
|
|
||||||
|
```text
|
||||||
|
../../../config.mjs
|
||||||
|
@astrojs/starlight/components
|
||||||
|
@opencode-ai/plugin
|
||||||
|
@opencode-ai/sdk
|
||||||
|
path
|
||||||
|
shescape
|
||||||
|
zod
|
||||||
|
|
||||||
|
@
|
||||||
|
@ai-sdk/anthropic
|
||||||
|
@ai-sdk/cerebras
|
||||||
|
@ai-sdk/google
|
||||||
|
@ai-sdk/openai
|
||||||
|
@ai-sdk/openai-compatible
|
||||||
|
@File#L37-42
|
||||||
|
@modelcontextprotocol/server-everything
|
||||||
|
@opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitHub owner/repo slugs referenced in docs
|
||||||
|
|
||||||
|
```text
|
||||||
|
24601/opencode-zellij-namer
|
||||||
|
angristan/opencode-wakatime
|
||||||
|
anomalyco/opencode
|
||||||
|
apps/opencode-agent
|
||||||
|
athal7/opencode-devcontainers
|
||||||
|
awesome-opencode/awesome-opencode
|
||||||
|
backnotprop/plannotator
|
||||||
|
ben-vargas/ai-sdk-provider-opencode-sdk
|
||||||
|
btriapitsyn/openchamber
|
||||||
|
BurntSushi/ripgrep
|
||||||
|
Cluster444/agentic
|
||||||
|
code-yeongyu/oh-my-opencode
|
||||||
|
darrenhinde/opencode-agents
|
||||||
|
different-ai/opencode-scheduler
|
||||||
|
different-ai/openwork
|
||||||
|
features/copilot
|
||||||
|
folke/tokyonight.nvim
|
||||||
|
franlol/opencode-md-table-formatter
|
||||||
|
ggml-org/llama.cpp
|
||||||
|
ghoulr/opencode-websearch-cited.git
|
||||||
|
H2Shami/opencode-helicone-session
|
||||||
|
hosenur/portal
|
||||||
|
jamesmurdza/daytona
|
||||||
|
jenslys/opencode-gemini-auth
|
||||||
|
JRedeker/opencode-morph-fast-apply
|
||||||
|
JRedeker/opencode-shell-strategy
|
||||||
|
kdcokenny/ocx
|
||||||
|
kdcokenny/opencode-background-agents
|
||||||
|
kdcokenny/opencode-notify
|
||||||
|
kdcokenny/opencode-workspace
|
||||||
|
kdcokenny/opencode-worktree
|
||||||
|
login/device
|
||||||
|
mohak34/opencode-notifier
|
||||||
|
morhetz/gruvbox
|
||||||
|
mtymek/opencode-obsidian
|
||||||
|
NeuralNomadsAI/CodeNomad
|
||||||
|
nick-vi/opencode-type-inject
|
||||||
|
NickvanDyke/opencode.nvim
|
||||||
|
NoeFabris/opencode-antigravity-auth
|
||||||
|
nordtheme/nord
|
||||||
|
numman-ali/opencode-openai-codex-auth
|
||||||
|
olimorris/codecompanion.nvim
|
||||||
|
panta82/opencode-notificator
|
||||||
|
rebelot/kanagawa.nvim
|
||||||
|
remorses/kimaki
|
||||||
|
sainnhe/everforest
|
||||||
|
shekohex/opencode-google-antigravity-auth
|
||||||
|
shekohex/opencode-pty.git
|
||||||
|
spoons-and-mirrors/subtask2
|
||||||
|
sudo-tee/opencode.nvim
|
||||||
|
supermemoryai/opencode-supermemory
|
||||||
|
Tarquinen/opencode-dynamic-context-pruning
|
||||||
|
Th3Whit3Wolf/one-nvim
|
||||||
|
upstash/context7
|
||||||
|
vtemian/micode
|
||||||
|
vtemian/octto
|
||||||
|
yetone/avante.nvim
|
||||||
|
zenobi-us/opencode-plugin-template
|
||||||
|
zenobi-us/opencode-skillful
|
||||||
|
```
|
||||||
|
|
||||||
|
## Paths, filenames, globs, and URLs
|
||||||
|
|
||||||
|
```text
|
||||||
|
./.opencode/themes/*.json
|
||||||
|
./<project-slug>/storage/
|
||||||
|
./config/#custom-directory
|
||||||
|
./global/storage/
|
||||||
|
.agents/skills/*/SKILL.md
|
||||||
|
.agents/skills/<name>/SKILL.md
|
||||||
|
.clang-format
|
||||||
|
.claude
|
||||||
|
.claude/skills
|
||||||
|
.claude/skills/*/SKILL.md
|
||||||
|
.claude/skills/<name>/SKILL.md
|
||||||
|
.env
|
||||||
|
.github/workflows/opencode.yml
|
||||||
|
.gitignore
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.ignore
|
||||||
|
.NET SDK
|
||||||
|
.npmrc
|
||||||
|
.ocamlformat
|
||||||
|
.opencode
|
||||||
|
.opencode/
|
||||||
|
.opencode/agents/
|
||||||
|
.opencode/commands/
|
||||||
|
.opencode/commands/test.md
|
||||||
|
.opencode/modes/
|
||||||
|
.opencode/plans/*.md
|
||||||
|
.opencode/plugins/
|
||||||
|
.opencode/skills/<name>/SKILL.md
|
||||||
|
.opencode/skills/git-release/SKILL.md
|
||||||
|
.opencode/tools/
|
||||||
|
.well-known/opencode
|
||||||
|
{ type: "raw" \| "patch", content: string }
|
||||||
|
{file:path/to/file}
|
||||||
|
**/*.js
|
||||||
|
%USERPROFILE%/intelephense/license.txt
|
||||||
|
%USERPROFILE%\.cache\opencode
|
||||||
|
%USERPROFILE%\.config\opencode\opencode.jsonc
|
||||||
|
%USERPROFILE%\.config\opencode\plugins
|
||||||
|
%USERPROFILE%\.local\share\opencode
|
||||||
|
%USERPROFILE%\.local\share\opencode\log
|
||||||
|
<project-root>/.opencode/themes/*.json
|
||||||
|
<providerId>/<modelId>
|
||||||
|
<your-project>/.opencode/plugins/
|
||||||
|
~
|
||||||
|
~/...
|
||||||
|
~/.agents/skills/*/SKILL.md
|
||||||
|
~/.agents/skills/<name>/SKILL.md
|
||||||
|
~/.aws/credentials
|
||||||
|
~/.bashrc
|
||||||
|
~/.cache/opencode
|
||||||
|
~/.cache/opencode/node_modules/
|
||||||
|
~/.claude/CLAUDE.md
|
||||||
|
~/.claude/skills/
|
||||||
|
~/.claude/skills/*/SKILL.md
|
||||||
|
~/.claude/skills/<name>/SKILL.md
|
||||||
|
~/.config/opencode
|
||||||
|
~/.config/opencode/AGENTS.md
|
||||||
|
~/.config/opencode/agents/
|
||||||
|
~/.config/opencode/commands/
|
||||||
|
~/.config/opencode/modes/
|
||||||
|
~/.config/opencode/opencode.json
|
||||||
|
~/.config/opencode/opencode.jsonc
|
||||||
|
~/.config/opencode/plugins/
|
||||||
|
~/.config/opencode/skills/*/SKILL.md
|
||||||
|
~/.config/opencode/skills/<name>/SKILL.md
|
||||||
|
~/.config/opencode/themes/*.json
|
||||||
|
~/.config/opencode/tools/
|
||||||
|
~/.config/zed/settings.json
|
||||||
|
~/.local/share
|
||||||
|
~/.local/share/opencode/
|
||||||
|
~/.local/share/opencode/auth.json
|
||||||
|
~/.local/share/opencode/log/
|
||||||
|
~/.local/share/opencode/mcp-auth.json
|
||||||
|
~/.local/share/opencode/opencode.jsonc
|
||||||
|
~/.npmrc
|
||||||
|
~/.zshrc
|
||||||
|
~/code/
|
||||||
|
~/Library/Application Support
|
||||||
|
~/projects/*
|
||||||
|
~/projects/personal/
|
||||||
|
${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts
|
||||||
|
$HOME/intelephense/license.txt
|
||||||
|
$HOME/projects/*
|
||||||
|
$XDG_CONFIG_HOME/opencode/themes/*.json
|
||||||
|
agent/
|
||||||
|
agents/
|
||||||
|
build/
|
||||||
|
commands/
|
||||||
|
dist/
|
||||||
|
http://<wsl-ip>:4096
|
||||||
|
http://127.0.0.1:8080/callback
|
||||||
|
http://localhost:<port>
|
||||||
|
http://localhost:4096
|
||||||
|
http://localhost:4096/doc
|
||||||
|
https://app.example.com
|
||||||
|
https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/
|
||||||
|
https://opencode.ai/zen/v1/chat/completions
|
||||||
|
https://opencode.ai/zen/v1/messages
|
||||||
|
https://opencode.ai/zen/v1/models/gemini-3-flash
|
||||||
|
https://opencode.ai/zen/v1/models/gemini-3-pro
|
||||||
|
https://opencode.ai/zen/v1/responses
|
||||||
|
https://RESOURCE_NAME.openai.azure.com/
|
||||||
|
laravel/pint
|
||||||
|
log/
|
||||||
|
model: "anthropic/claude-sonnet-4-5"
|
||||||
|
modes/
|
||||||
|
node_modules/
|
||||||
|
openai/gpt-4.1
|
||||||
|
opencode.ai/config.json
|
||||||
|
opencode/<model-id>
|
||||||
|
opencode/gpt-5.1-codex
|
||||||
|
opencode/gpt-5.2-codex
|
||||||
|
opencode/kimi-k2
|
||||||
|
openrouter/google/gemini-2.5-flash
|
||||||
|
opncd.ai/s/<share-id>
|
||||||
|
packages/*/AGENTS.md
|
||||||
|
plugins/
|
||||||
|
project/
|
||||||
|
provider_id/model_id
|
||||||
|
provider/model
|
||||||
|
provider/model-id
|
||||||
|
rm -rf ~/.cache/opencode
|
||||||
|
skills/
|
||||||
|
skills/*/SKILL.md
|
||||||
|
src/**/*.ts
|
||||||
|
themes/
|
||||||
|
tools/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keybind strings
|
||||||
|
|
||||||
|
```text
|
||||||
|
alt+b
|
||||||
|
Alt+Ctrl+K
|
||||||
|
alt+d
|
||||||
|
alt+f
|
||||||
|
Cmd+Esc
|
||||||
|
Cmd+Option+K
|
||||||
|
Cmd+Shift+Esc
|
||||||
|
Cmd+Shift+G
|
||||||
|
Cmd+Shift+P
|
||||||
|
ctrl+a
|
||||||
|
ctrl+b
|
||||||
|
ctrl+d
|
||||||
|
ctrl+e
|
||||||
|
Ctrl+Esc
|
||||||
|
ctrl+f
|
||||||
|
ctrl+g
|
||||||
|
ctrl+k
|
||||||
|
Ctrl+Shift+Esc
|
||||||
|
Ctrl+Shift+P
|
||||||
|
ctrl+t
|
||||||
|
ctrl+u
|
||||||
|
ctrl+w
|
||||||
|
ctrl+x
|
||||||
|
DELETE
|
||||||
|
Shift+Enter
|
||||||
|
WIN+R
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model ID strings referenced
|
||||||
|
|
||||||
|
```text
|
||||||
|
{env:OPENCODE_MODEL}
|
||||||
|
anthropic/claude-3-5-sonnet-20241022
|
||||||
|
anthropic/claude-haiku-4-20250514
|
||||||
|
anthropic/claude-haiku-4-5
|
||||||
|
anthropic/claude-sonnet-4-20250514
|
||||||
|
anthropic/claude-sonnet-4-5
|
||||||
|
gitlab/duo-chat-haiku-4-5
|
||||||
|
lmstudio/google/gemma-3n-e4b
|
||||||
|
openai/gpt-4.1
|
||||||
|
openai/gpt-5
|
||||||
|
opencode/gpt-5.1-codex
|
||||||
|
opencode/gpt-5.2-codex
|
||||||
|
opencode/kimi-k2
|
||||||
|
openrouter/google/gemini-2.5-flash
|
||||||
|
```
|
||||||
+112
-15
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
mode: primary
|
mode: primary
|
||||||
hidden: true
|
hidden: true
|
||||||
model: opencode/gpt-5.4-nano
|
model: opencode/minimax-m2.5
|
||||||
color: "#44BA81"
|
color: "#44BA81"
|
||||||
tools:
|
tools:
|
||||||
"*": false
|
"*": false
|
||||||
@@ -14,30 +14,127 @@ Use your github-triage tool to triage issues.
|
|||||||
|
|
||||||
This file is the source of truth for ownership/routing rules.
|
This file is the source of truth for ownership/routing rules.
|
||||||
|
|
||||||
Assign issues by choosing the team with the strongest overlap. The github-triage tool will assign a random member from that team.
|
## Labels
|
||||||
|
|
||||||
Do not add labels to issues. Only assign an owner.
|
### windows
|
||||||
|
|
||||||
When calling github-triage, pass one of these team values: tui, desktop_web, core, inference, windows.
|
Use for any issue that mentions Windows (the OS). Be sure they are saying that they are on Windows.
|
||||||
|
|
||||||
## Teams
|
- Use if they mention WSL too
|
||||||
|
|
||||||
### TUI
|
#### perf
|
||||||
|
|
||||||
Terminal UI issues, including rendering, keybindings, scrolling, terminal compatibility, SSH behavior, crashes in the TUI, and low-level TUI performance.
|
Performance-related issues:
|
||||||
|
|
||||||
### Desktop / Web
|
- Slow performance
|
||||||
|
- High RAM usage
|
||||||
|
- High CPU usage
|
||||||
|
|
||||||
Desktop application and browser-based app issues, including `opencode web`, desktop-specific UI behavior, packaging, and web view problems.
|
**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness.
|
||||||
|
|
||||||
### Core
|
#### desktop
|
||||||
|
|
||||||
Core opencode server and harness issues, including sqlite, snapshots, memory, API behavior, agent context construction, tool execution, provider integrations, model behavior, documentation, and larger architectural features.
|
Desktop app issues:
|
||||||
|
|
||||||
### Inference
|
- `opencode web` command
|
||||||
|
- The desktop app itself
|
||||||
|
|
||||||
OpenCode Zen, OpenCode Go, and billing issues.
|
**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues.
|
||||||
|
|
||||||
### Windows
|
#### nix
|
||||||
|
|
||||||
Windows-specific issues, including native Windows behavior, WSL interactions, path handling, shell compatibility, and installation or runtime problems that only happen on Windows.
|
**Only** add if the issue explicitly mentions nix.
|
||||||
|
|
||||||
|
If the issue does not mention nix, do not add nix.
|
||||||
|
|
||||||
|
If the issue mentions nix, assign to `rekram1-node`.
|
||||||
|
|
||||||
|
#### zen
|
||||||
|
|
||||||
|
**Only** add if the issue mentions "zen" or "opencode zen" or "opencode black".
|
||||||
|
|
||||||
|
If the issue doesn't have "zen" or "opencode black" in it then don't add zen label
|
||||||
|
|
||||||
|
#### core
|
||||||
|
|
||||||
|
Use for core server issues in `packages/opencode/`, excluding `packages/opencode/src/cli/cmd/tui/`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- LSP server behavior
|
||||||
|
- Harness behavior (agent + tools)
|
||||||
|
- Feature requests for server behavior
|
||||||
|
- Agent context construction
|
||||||
|
- API endpoints
|
||||||
|
- Provider integration issues
|
||||||
|
- New, broken, or poor-quality models
|
||||||
|
|
||||||
|
#### acp
|
||||||
|
|
||||||
|
If the issue mentions acp support, assign acp label.
|
||||||
|
|
||||||
|
#### docs
|
||||||
|
|
||||||
|
Add if the issue requests better documentation or docs updates.
|
||||||
|
|
||||||
|
#### opentui
|
||||||
|
|
||||||
|
TUI issues potentially caused by our underlying TUI library:
|
||||||
|
|
||||||
|
- Keybindings not working
|
||||||
|
- Scroll speed issues (too fast/slow/laggy)
|
||||||
|
- Screen flickering
|
||||||
|
- Crashes with opentui in the log
|
||||||
|
|
||||||
|
**Do not** add for general TUI bugs.
|
||||||
|
|
||||||
|
When assigning to people here are the following rules:
|
||||||
|
|
||||||
|
Desktop / Web:
|
||||||
|
Use for desktop-labeled issues only.
|
||||||
|
|
||||||
|
- adamdotdevin
|
||||||
|
- iamdavidhill
|
||||||
|
- Brendonovich
|
||||||
|
- nexxeln
|
||||||
|
|
||||||
|
Zen:
|
||||||
|
ONLY assign if the issue will have the "zen" label.
|
||||||
|
|
||||||
|
- fwang
|
||||||
|
- MrMushrooooom
|
||||||
|
|
||||||
|
TUI (`packages/opencode/src/cli/cmd/tui/...`):
|
||||||
|
|
||||||
|
- thdxr for TUI UX/UI product decisions and interaction flow
|
||||||
|
- kommander for OpenTUI engine issues: rendering artifacts, keybind handling, terminal compatibility, SSH behavior, and low-level perf bottlenecks
|
||||||
|
- rekram1-node for TUI bugs that are not clearly OpenTUI engine issues
|
||||||
|
|
||||||
|
Core (`packages/opencode/...`, excluding TUI subtree):
|
||||||
|
|
||||||
|
- thdxr for sqlite/snapshot/memory bugs and larger architectural core features
|
||||||
|
- jlongster for opencode server + API feature work (tool currently remaps jlongster -> thdxr until assignable)
|
||||||
|
- rekram1-node for harness issues, provider issues, and other bug-squashing
|
||||||
|
|
||||||
|
For core bugs that do not clearly map, either thdxr or rekram1-node is acceptable.
|
||||||
|
|
||||||
|
Docs:
|
||||||
|
|
||||||
|
- R44VC0RP
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
- Hona (assign any issue that mentions Windows or is likely Windows-specific)
|
||||||
|
|
||||||
|
Determinism rules:
|
||||||
|
|
||||||
|
- If title + body does not contain "zen", do not add the "zen" label
|
||||||
|
- If "nix" label is added but title + body does not mention nix/nixos, the tool will drop "nix"
|
||||||
|
- If title + body mentions nix/nixos, assign to `rekram1-node`
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
ACP:
|
||||||
|
|
||||||
|
- rekram1-node (assign any acp issues to rekram1-node)
|
||||||
|
|||||||
@@ -18,12 +18,9 @@ Do not use `git log` or author metadata when deciding attribution.
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Write the final file with release sections in this order:
|
- Write the final file with sections in this order:
|
||||||
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
|
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
|
||||||
- Only include sections that have at least one notable entry
|
- Only include sections that have at least one notable entry
|
||||||
- Within each release section, keep bug fixes grouped under `### Bugfixes`
|
|
||||||
- Keep other notable entries under `### Improvements` when a section has bug fixes too
|
|
||||||
- Omit empty subsections
|
|
||||||
- Keep one bullet per commit you keep
|
- Keep one bullet per commit you keep
|
||||||
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
|
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
|
||||||
- Start each bullet with a capital letter
|
- Start each bullet with a capital letter
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
description: translate English to other languages
|
|
||||||
model: opencode/claude-opus-4-7
|
|
||||||
---
|
|
||||||
|
|
||||||
run git diff and translate changed english doc and UI copy files to other international languages. Translate all languages in parallel to save time.
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
|
||||||
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
|
||||||
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
|
||||||
- Also apply locale-specific guidance from `.opencode/glossary/<locale>.md` when available (for example, `zh-cn.md`).
|
|
||||||
- Do not modify fenced code blocks.
|
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"provider": {},
|
"provider": {
|
||||||
|
"opencode": {
|
||||||
|
"options": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
"edit": {
|
"edit": {
|
||||||
"packages/opencode/migration/*": "deny",
|
"packages/opencode/migration/*": "deny",
|
||||||
|
|||||||
@@ -1,38 +1,21 @@
|
|||||||
---
|
---
|
||||||
name: effect
|
name: effect
|
||||||
description: Work with Effect v4 / effect-smol TypeScript code in this repo
|
description: Answer questions about the Effect framework
|
||||||
---
|
---
|
||||||
|
|
||||||
# Effect
|
# Effect
|
||||||
|
|
||||||
This codebase uses Effect for typed, composable TypeScript services, schemas, and workflows.
|
This codebase uses Effect, a framework for writing typescript.
|
||||||
|
|
||||||
## Source Of Truth
|
## How to Answer Effect Questions
|
||||||
|
|
||||||
Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples.
|
1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to
|
||||||
|
`.opencode/references/effect-smol` in this project NOT the skill folder.
|
||||||
1. If `.opencode/references/effect-smol` is missing, clone `https://github.com/Effect-TS/effect-smol` there. Do this in the project, not in the skill folder.
|
2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts
|
||||||
2. Search `.opencode/references/effect-smol` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code.
|
3. Provide responses based on the actual Effect source code and documentation
|
||||||
3. Also inspect existing repo code for local house style before introducing new patterns.
|
|
||||||
4. Prefer answers and implementations backed by specific source files or nearby repo examples.
|
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
- Prefer current Effect v4 APIs and project-local patterns over old blog posts, examples, or package-memory guesses.
|
- Always use the explore agent with the cloned repository when answering Effect-related questions
|
||||||
- Use `Effect.gen(function* () { ... })` for multi-step workflows.
|
- Reference specific files and patterns found in the Effect codebase
|
||||||
- Use `Effect.fn("Name")` or `Effect.fnUntraced(...)` for named effects when adding reusable service methods or important workflows.
|
- Do not answer from memory - always verify against the source
|
||||||
- Prefer Effect `Schema` for API and domain data shapes. Use branded schemas for IDs and `Schema.TaggedErrorClass` for typed domain errors when modeling new error surfaces.
|
|
||||||
- Keep HTTP handlers thin: decode input, read request context, call services, and map transport errors. Put business rules in services.
|
|
||||||
- In Effect service code, prefer Effect-aware platform abstractions and dependencies over ad hoc promises where the surrounding code already does so.
|
|
||||||
- Keep layer composition explicit. Avoid broad hidden provisioning that makes missing dependencies hard to see.
|
|
||||||
- In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior.
|
|
||||||
- Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types.
|
|
||||||
- Do not answer from memory. Verify against `.opencode/references/effect-smol` or nearby code first.
|
|
||||||
|
|
||||||
## Testing Patterns
|
|
||||||
|
|
||||||
- Use `testEffect(...)` from `packages/opencode/test/lib/effect.ts` for tests that exercise Effect services, layers, runtime context, scoped resources, or platform integrations.
|
|
||||||
- Use `it.live(...)` for filesystem, git repositories, HTTP servers, sockets, child processes, locks, real time, and other live platform behavior.
|
|
||||||
- Run tests from package directories such as `packages/opencode`; never run package tests from the repo root.
|
|
||||||
- Prefer explicit test layers over ad hoc managed runtimes. Keep dependency provisioning visible in the test file.
|
|
||||||
- Use scoped fixtures and finalizers for resources that must be cleaned up, including temporary directories, flags, databases, fibers, servers, and global state.
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
/// <reference path="../env.d.ts" />
|
/// <reference path="../env.d.ts" />
|
||||||
import { tool } from "@opencode-ai/plugin"
|
import { tool } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
const TEAM = {
|
const TEAM = {
|
||||||
tui: ["kommander", "simonklee"],
|
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
||||||
desktop_web: ["Hona", "Brendonovich"],
|
zen: ["fwang", "MrMushrooooom"],
|
||||||
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
|
tui: ["thdxr", "kommander", "rekram1-node"],
|
||||||
inference: ["fwang", "MrMushrooooom"],
|
core: ["thdxr", "rekram1-node", "jlongster"],
|
||||||
|
docs: ["R44VC0RP"],
|
||||||
windows: ["Hona"],
|
windows: ["Hona"],
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
const ASSIGNEES = [...new Set(Object.values(TEAM).flat())]
|
||||||
|
|
||||||
function pick<T>(items: readonly T[]) {
|
function pick<T>(items: readonly T[]) {
|
||||||
return items[Math.floor(Math.random() * items.length)]!
|
return items[Math.floor(Math.random() * items.length)]!
|
||||||
}
|
}
|
||||||
@@ -36,25 +38,79 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description: `Use this tool to assign a GitHub issue.
|
description: `Use this tool to assign and/or label a GitHub issue.
|
||||||
|
|
||||||
Provide the team that should own the issue. This tool picks a random assignee from that team and does not apply labels.`,
|
Choose labels and assignee using the current triage policy and ownership rules.
|
||||||
|
Pick the most fitting labels for the issue and assign one owner.
|
||||||
|
|
||||||
|
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`,
|
||||||
args: {
|
args: {
|
||||||
team: tool.schema
|
assignee: tool.schema
|
||||||
.enum(Object.keys(TEAM) as [keyof typeof TEAM, ...(keyof typeof TEAM)[]])
|
.enum(ASSIGNEES as [string, ...string[]])
|
||||||
.describe("The owning team"),
|
.describe("The username of the assignee")
|
||||||
|
.default("rekram1-node"),
|
||||||
|
labels: tool.schema
|
||||||
|
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
|
||||||
|
.describe("The labels(s) to add to the issue")
|
||||||
|
.default([]),
|
||||||
},
|
},
|
||||||
async execute(args) {
|
async execute(args) {
|
||||||
const issue = getIssueNumber()
|
const issue = getIssueNumber()
|
||||||
const owner = "anomalyco"
|
const owner = "anomalyco"
|
||||||
const repo = "opencode"
|
const repo = "opencode"
|
||||||
const assignee = pick(TEAM[args.team])
|
|
||||||
|
const results: string[] = []
|
||||||
|
let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))]
|
||||||
|
const web = labels.includes("web")
|
||||||
|
const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase()
|
||||||
|
const zen = /\bzen\b/.test(text) || text.includes("opencode black")
|
||||||
|
const nix = /\bnix(os)?\b/.test(text)
|
||||||
|
|
||||||
|
if (labels.includes("nix") && !nix) {
|
||||||
|
labels = labels.filter((x) => x !== "nix")
|
||||||
|
results.push("Dropped label: nix (issue does not mention nix)")
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
|
||||||
|
|
||||||
|
if (labels.includes("zen") && !zen) {
|
||||||
|
throw new Error("Only add the zen label when issue title/body contains 'zen'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) {
|
||||||
|
throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) {
|
||||||
|
throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignee === "Hona" && !labels.includes("windows")) {
|
||||||
|
throw new Error("Only windows issues should be assigned to Hona")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignee === "R44VC0RP" && !labels.includes("docs")) {
|
||||||
|
throw new Error("Only docs issues should be assigned to R44VC0RP")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignee === "kommander" && !labels.includes("opentui")) {
|
||||||
|
throw new Error("Only opentui issues should be assigned to kommander")
|
||||||
|
}
|
||||||
|
|
||||||
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
|
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ assignees: [assignee] }),
|
body: JSON.stringify({ assignees: [assignee] }),
|
||||||
})
|
})
|
||||||
|
results.push(`Assigned @${assignee} to issue #${issue}`)
|
||||||
|
|
||||||
return `Assigned @${assignee} from ${args.team} to issue #${issue}`
|
if (labels.length > 0) {
|
||||||
|
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ labels }),
|
||||||
|
})
|
||||||
|
results.push(`Added labels: ${labels.join(", ")}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.join("\n")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
- Use Bun APIs when possible, like `Bun.file()`
|
- Use Bun APIs when possible, like `Bun.file()`
|
||||||
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
||||||
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
||||||
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
|
|
||||||
|
|
||||||
Reduce total variable count by inlining when a value is only used once.
|
Reduce total variable count by inlining when a value is only used once.
|
||||||
|
|
||||||
|
|||||||
+18
-6
@@ -73,7 +73,7 @@ Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
|
|||||||
- `packages/opencode`: OpenCode core business logic & server.
|
- `packages/opencode`: OpenCode core business logic & server.
|
||||||
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
||||||
- `packages/app`: The shared web UI components, written in SolidJS
|
- `packages/app`: The shared web UI components, written in SolidJS
|
||||||
- `packages/desktop`: The native desktop app, built with Electron (wraps `packages/app`)
|
- `packages/desktop`: The native desktop app, built with Tauri (wraps `packages/app`)
|
||||||
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
||||||
|
|
||||||
### Understanding bun dev vs opencode
|
### Understanding bun dev vs opencode
|
||||||
@@ -123,21 +123,33 @@ This starts a local dev server at http://localhost:5173 (or similar port shown i
|
|||||||
|
|
||||||
### Running the Desktop App
|
### Running the Desktop App
|
||||||
|
|
||||||
The desktop app is an Electron application that wraps the web UI.
|
The desktop app is a native Tauri application that wraps the web UI.
|
||||||
|
|
||||||
To run the desktop app in development:
|
To run the native desktop app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd packages/desktop tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the web dev server on http://localhost:1420 and opens the native window.
|
||||||
|
|
||||||
|
If you only want the web dev server (no native shell):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run --cwd packages/desktop dev
|
bun run --cwd packages/desktop dev
|
||||||
```
|
```
|
||||||
|
|
||||||
To create a production build and package the app:
|
To create a production `dist/` and build the native app bundle:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run --cwd packages/desktop build
|
bun run --cwd packages/desktop tauri build
|
||||||
bun run --cwd packages/desktop package
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This runs `bun run --cwd packages/desktop build` automatically via Tauri’s `beforeBuildCommand`.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
|
> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث
|
|||||||
|
|
||||||
يتوفر OpenCode ايضا كتطبيق سطح مكتب. قم بالتنزيل مباشرة من [صفحة الاصدارات](https://github.com/anomalyco/opencode/releases) او من [opencode.ai/download](https://opencode.ai/download).
|
يتوفر OpenCode ايضا كتطبيق سطح مكتب. قم بالتنزيل مباشرة من [صفحة الاصدارات](https://github.com/anomalyco/opencode/releases) او من [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| المنصة | التنزيل |
|
| المنصة | التنزيل |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb` او `.rpm` او AppImage |
|
| Linux | `.deb` او `.rpm` او AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev
|
|||||||
|
|
||||||
OpenCode ডেস্কটপ অ্যাপ্লিকেশন হিসেবেও উপলব্ধ। সরাসরি [রিলিজ পেজ](https://github.com/anomalyco/opencode/releases) অথবা [opencode.ai/download](https://opencode.ai/download) থেকে ডাউনলোড করুন।
|
OpenCode ডেস্কটপ অ্যাপ্লিকেশন হিসেবেও উপলব্ধ। সরাসরি [রিলিজ পেজ](https://github.com/anomalyco/opencode/releases) অথবা [opencode.ai/download](https://opencode.ai/download) থেকে ডাউনলোড করুন।
|
||||||
|
|
||||||
| প্ল্যাটফর্ম | ডাউনলোড |
|
| প্ল্যাটফর্ম | ডাউনলোড |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, or `.AppImage` |
|
| Linux | `.deb`, `.rpm`, or AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ou github:anomalyco/opencode para a branch
|
|||||||
|
|
||||||
O OpenCode também está disponível como aplicativo desktop. Baixe diretamente pela [página de releases](https://github.com/anomalyco/opencode/releases) ou em [opencode.ai/download](https://opencode.ai/download).
|
O OpenCode também está disponível como aplicativo desktop. Baixe diretamente pela [página de releases](https://github.com/anomalyco/opencode/releases) ou em [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Plataforma | Download |
|
| Plataforma | Download |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm` ou AppImage |
|
| Linux | `.deb`, `.rpm` ou AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ili github:anomalyco/opencode za najnoviji
|
|||||||
|
|
||||||
OpenCode je dostupan i kao desktop aplikacija. Preuzmi je direktno sa [stranice izdanja](https://github.com/anomalyco/opencode/releases) ili sa [opencode.ai/download](https://opencode.ai/download).
|
OpenCode je dostupan i kao desktop aplikacija. Preuzmi je direktno sa [stranice izdanja](https://github.com/anomalyco/opencode/releases) ili sa [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Platforma | Preuzimanje |
|
| Platforma | Preuzimanje |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, ili AppImage |
|
| Linux | `.deb`, `.rpm`, ili AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste
|
|||||||
|
|
||||||
OpenCode findes også som desktop-app. Download direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download).
|
OpenCode findes også som desktop-app. Download direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Platform | Download |
|
| Platform | Download |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, eller AppImage |
|
| Linux | `.deb`, `.rpm`, eller AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # oder github:anomalyco/opencode für den neu
|
|||||||
|
|
||||||
OpenCode ist auch als Desktop-Anwendung verfügbar. Lade sie direkt von der [Releases-Seite](https://github.com/anomalyco/opencode/releases) oder [opencode.ai/download](https://opencode.ai/download) herunter.
|
OpenCode ist auch als Desktop-Anwendung verfügbar. Lade sie direkt von der [Releases-Seite](https://github.com/anomalyco/opencode/releases) oder [opencode.ai/download](https://opencode.ai/download) herunter.
|
||||||
|
|
||||||
| Plattform | Download |
|
| Plattform | Download |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm` oder AppImage |
|
| Linux | `.deb`, `.rpm` oder AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # o github:anomalyco/opencode para la rama de
|
|||||||
|
|
||||||
OpenCode también está disponible como aplicación de escritorio. Descárgala directamente desde la [página de releases](https://github.com/anomalyco/opencode/releases) o desde [opencode.ai/download](https://opencode.ai/download).
|
OpenCode también está disponible como aplicación de escritorio. Descárgala directamente desde la [página de releases](https://github.com/anomalyco/opencode/releases) o desde [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Plataforma | Descarga |
|
| Plataforma | Descarga |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, o AppImage |
|
| Linux | `.deb`, `.rpm`, o AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ou github:anomalyco/opencode pour la branch
|
|||||||
|
|
||||||
OpenCode est aussi disponible en application de bureau. Téléchargez-la directement depuis la [page des releases](https://github.com/anomalyco/opencode/releases) ou [opencode.ai/download](https://opencode.ai/download).
|
OpenCode est aussi disponible en application de bureau. Téléchargez-la directement depuis la [page des releases](https://github.com/anomalyco/opencode/releases) ou [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Plateforme | Téléchargement |
|
| Plateforme | Téléchargement |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, ou AppImage |
|
| Linux | `.deb`, `.rpm`, ou AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ή github:anomalyco/opencode με βάση
|
|||||||
|
|
||||||
Το OpenCode είναι επίσης διαθέσιμο ως εφαρμογή. Κατέβασε το απευθείας από τη [σελίδα εκδόσεων](https://github.com/anomalyco/opencode/releases) ή το [opencode.ai/download](https://opencode.ai/download).
|
Το OpenCode είναι επίσης διαθέσιμο ως εφαρμογή. Κατέβασε το απευθείας από τη [σελίδα εκδόσεων](https://github.com/anomalyco/opencode/releases) ή το [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Πλατφόρμα | Λήψη |
|
| Πλατφόρμα | Λήψη |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, ή AppImage |
|
| Linux | `.deb`, `.rpm`, ή AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # oppure github:anomalyco/opencode per l’ul
|
|||||||
|
|
||||||
OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla direttamente dalla [pagina delle release](https://github.com/anomalyco/opencode/releases) oppure da [opencode.ai/download](https://opencode.ai/download).
|
OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla direttamente dalla [pagina delle release](https://github.com/anomalyco/opencode/releases) oppure da [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Piattaforma | Download |
|
| Piattaforma | Download |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, oppure AppImage |
|
| Linux | `.deb`, `.rpm`, oppure AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # または github:anomalyco/opencode で最
|
|||||||
|
|
||||||
OpenCode はデスクトップアプリとしても利用できます。[releases page](https://github.com/anomalyco/opencode/releases) から直接ダウンロードするか、[opencode.ai/download](https://opencode.ai/download) を利用してください。
|
OpenCode はデスクトップアプリとしても利用できます。[releases page](https://github.com/anomalyco/opencode/releases) から直接ダウンロードするか、[opencode.ai/download](https://opencode.ai/download) を利用してください。
|
||||||
|
|
||||||
| プラットフォーム | ダウンロード |
|
| プラットフォーム | ダウンロード |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`、`.rpm`、または AppImage |
|
| Linux | `.deb`、`.rpm`、または AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # 또는 github:anomalyco/opencode 로 최신
|
|||||||
|
|
||||||
OpenCode 는 데스크톱 앱으로도 제공됩니다. [releases page](https://github.com/anomalyco/opencode/releases) 에서 직접 다운로드하거나 [opencode.ai/download](https://opencode.ai/download) 를 이용하세요.
|
OpenCode 는 데스크톱 앱으로도 제공됩니다. [releases page](https://github.com/anomalyco/opencode/releases) 에서 직접 다운로드하거나 [opencode.ai/download](https://opencode.ai/download) 를 이용하세요.
|
||||||
|
|
||||||
| 플랫폼 | 다운로드 |
|
| 플랫폼 | 다운로드 |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, 또는 AppImage |
|
| Linux | `.deb`, `.rpm`, 또는 AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev
|
|||||||
|
|
||||||
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/anomalyco/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
|
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/anomalyco/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Platform | Download |
|
| Platform | Download |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, or `.AppImage` |
|
| Linux | `.deb`, `.rpm`, or AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
@@ -132,7 +132,7 @@ It's very similar to Claude Code in terms of capability. Here are the key differ
|
|||||||
|
|
||||||
- 100% open source
|
- 100% open source
|
||||||
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
||||||
- Built-in opt-in LSP support
|
- Out-of-the-box LSP support
|
||||||
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||||
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
|
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste
|
|||||||
|
|
||||||
OpenCode er også tilgjengelig som en desktop-app. Last ned direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download).
|
OpenCode er også tilgjengelig som en desktop-app. Last ned direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Plattform | Nedlasting |
|
| Plattform | Nedlasting |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm` eller AppImage |
|
| Linux | `.deb`, `.rpm` eller AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # lub github:anomalyco/opencode dla najnowsze
|
|||||||
|
|
||||||
OpenCode jest także dostępny jako aplikacja desktopowa. Pobierz ją bezpośrednio ze strony [releases](https://github.com/anomalyco/opencode/releases) lub z [opencode.ai/download](https://opencode.ai/download).
|
OpenCode jest także dostępny jako aplikacja desktopowa. Pobierz ją bezpośrednio ze strony [releases](https://github.com/anomalyco/opencode/releases) lub z [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Platforma | Pobieranie |
|
| Platforma | Pobieranie |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm` lub AppImage |
|
| Linux | `.deb`, `.rpm` lub AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # или github:anomalyco/opencode для с
|
|||||||
|
|
||||||
OpenCode также доступен как десктопное приложение. Скачайте его со [страницы релизов](https://github.com/anomalyco/opencode/releases) или с [opencode.ai/download](https://opencode.ai/download).
|
OpenCode также доступен как десктопное приложение. Скачайте его со [страницы релизов](https://github.com/anomalyco/opencode/releases) или с [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Платформа | Загрузка |
|
| Платформа | Загрузка |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm` или AppImage |
|
| Linux | `.deb`, `.rpm` или AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # หรือ github:anomalyco/opencode ส
|
|||||||
|
|
||||||
OpenCode มีให้ใช้งานเป็นแอปพลิเคชันเดสก์ท็อป ดาวน์โหลดโดยตรงจาก [หน้ารุ่น](https://github.com/anomalyco/opencode/releases) หรือ [opencode.ai/download](https://opencode.ai/download)
|
OpenCode มีให้ใช้งานเป็นแอปพลิเคชันเดสก์ท็อป ดาวน์โหลดโดยตรงจาก [หน้ารุ่น](https://github.com/anomalyco/opencode/releases) หรือ [opencode.ai/download](https://opencode.ai/download)
|
||||||
|
|
||||||
| แพลตฟอร์ม | ดาวน์โหลด |
|
| แพลตฟอร์ม | ดาวน์โหลด |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, หรือ AppImage |
|
| Linux | `.deb`, `.rpm`, หรือ AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # veya en güncel geliştirme dalı için git
|
|||||||
|
|
||||||
OpenCode ayrıca masaüstü uygulaması olarak da mevcuttur. Doğrudan [sürüm sayfasından](https://github.com/anomalyco/opencode/releases) veya [opencode.ai/download](https://opencode.ai/download) adresinden indirebilirsiniz.
|
OpenCode ayrıca masaüstü uygulaması olarak da mevcuttur. Doğrudan [sürüm sayfasından](https://github.com/anomalyco/opencode/releases) veya [opencode.ai/download](https://opencode.ai/download) adresinden indirebilirsiniz.
|
||||||
|
|
||||||
| Platform | İndirme |
|
| Platform | İndirme |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm` veya AppImage |
|
| Linux | `.deb`, `.rpm` veya AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # або github:anomalyco/opencode для н
|
|||||||
|
|
||||||
OpenCode також доступний як десктопний застосунок. Завантажуйте напряму зі [сторінки релізів](https://github.com/anomalyco/opencode/releases) або [opencode.ai/download](https://opencode.ai/download).
|
OpenCode також доступний як десктопний застосунок. Завантажуйте напряму зі [сторінки релізів](https://github.com/anomalyco/opencode/releases) або [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Платформа | Завантаження |
|
| Платформа | Завантаження |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm` або AppImage |
|
| Linux | `.deb`, `.rpm` або AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # hoặc github:anomalyco/opencode cho nhánh
|
|||||||
|
|
||||||
OpenCode cũng có sẵn dưới dạng ứng dụng desktop. Tải trực tiếp từ [trang releases](https://github.com/anomalyco/opencode/releases) hoặc [opencode.ai/download](https://opencode.ai/download).
|
OpenCode cũng có sẵn dưới dạng ứng dụng desktop. Tải trực tiếp từ [trang releases](https://github.com/anomalyco/opencode/releases) hoặc [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
| Nền tảng | Tải xuống |
|
| Nền tảng | Tải xuống |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, hoặc AppImage |
|
| Linux | `.deb`, `.rpm`, hoặc AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew)
|
# macOS (Homebrew)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最
|
|||||||
|
|
||||||
OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下载。
|
OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下载。
|
||||||
|
|
||||||
| 平台 | 下载文件 |
|
| 平台 | 下载文件 |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`、`.rpm` 或 AppImage |
|
| Linux | `.deb`、`.rpm` 或 AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew Cask)
|
# macOS (Homebrew Cask)
|
||||||
|
|||||||
+6
-6
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取
|
|||||||
|
|
||||||
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
|
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
|
||||||
|
|
||||||
| 平台 | 下載連結 |
|
| 平台 | 下載連結 |
|
||||||
| --------------------- | ---------------------------------- |
|
| --------------------- | ------------------------------------- |
|
||||||
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
| Linux | `.deb`, `.rpm`, 或 AppImage |
|
| Linux | `.deb`, `.rpm`, 或 AppImage |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Homebrew Cask)
|
# macOS (Homebrew Cask)
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776683584,
|
"lastModified": 1773909469,
|
||||||
"narHash": "sha256-NuTLMrr10Tng72hurYG8jYQ4XKK8wnpJmOGcPiis96g=",
|
"narHash": "sha256-vglVrLfHjFIzIdV9A27Ugul6rh3I1qHbbitGW7dk420=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9dd5558b06dbdacbf635a3dd36dce1b1a7ee3a89",
|
"rev": "7149c06513f335be57f26fcbbbe34afda923882b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
+1
-29
@@ -115,27 +115,6 @@ const zenLiteCouponFirstMonth100 = new stripe.Coupon("ZenLiteCouponFirstMonth100
|
|||||||
appliesToProducts: [zenLiteProduct.id],
|
appliesToProducts: [zenLiteProduct.id],
|
||||||
duration: "once",
|
duration: "once",
|
||||||
})
|
})
|
||||||
const zenLiteCouponThreeMonths100 = new stripe.Coupon("ZenLiteCoupon3Months100", {
|
|
||||||
name: "3 months 100% off",
|
|
||||||
percentOff: 100,
|
|
||||||
appliesToProducts: [zenLiteProduct.id],
|
|
||||||
duration: "repeating",
|
|
||||||
durationInMonths: 3,
|
|
||||||
})
|
|
||||||
const zenLiteCouponSixMonths100 = new stripe.Coupon("ZenLiteCoupon6Months100", {
|
|
||||||
name: "6 months 100% off",
|
|
||||||
percentOff: 100,
|
|
||||||
appliesToProducts: [zenLiteProduct.id],
|
|
||||||
duration: "repeating",
|
|
||||||
durationInMonths: 6,
|
|
||||||
})
|
|
||||||
const zenLiteCouponTwelveMonths100 = new stripe.Coupon("ZenLiteCoupon12Months100", {
|
|
||||||
name: "12 months 100% off",
|
|
||||||
percentOff: 100,
|
|
||||||
appliesToProducts: [zenLiteProduct.id],
|
|
||||||
duration: "repeating",
|
|
||||||
durationInMonths: 12,
|
|
||||||
})
|
|
||||||
const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
||||||
product: zenLiteProduct.id,
|
product: zenLiteProduct.id,
|
||||||
currency: "usd",
|
currency: "usd",
|
||||||
@@ -152,9 +131,6 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
|
|||||||
priceInr: 92900,
|
priceInr: 92900,
|
||||||
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
||||||
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
|
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
|
||||||
threeMonths100Coupon: zenLiteCouponThreeMonths100.id,
|
|
||||||
sixMonths100Coupon: zenLiteCouponSixMonths100.id,
|
|
||||||
twelveMonths100Coupon: zenLiteCouponTwelveMonths100.id,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -221,9 +197,6 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
|||||||
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
||||||
properties: { value: stripeWebhook.secret },
|
properties: { value: stripeWebhook.secret },
|
||||||
})
|
})
|
||||||
const INCIDENT_WEBHOOK_SIGNING_SECRET = new sst.Secret("INCIDENT_WEBHOOK_SIGNING_SECRET")
|
|
||||||
const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL")
|
|
||||||
|
|
||||||
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
@@ -254,8 +227,6 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
database,
|
database,
|
||||||
AUTH_API_URL,
|
AUTH_API_URL,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET,
|
||||||
INCIDENT_WEBHOOK_SIGNING_SECRET,
|
|
||||||
DISCORD_INCIDENT_WEBHOOK_URL,
|
|
||||||
STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY,
|
||||||
EMAILOCTOPUS_API_KEY,
|
EMAILOCTOPUS_API_KEY,
|
||||||
AWS_SES_ACCESS_KEY_ID,
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
@@ -265,6 +236,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
SALESFORCE_INSTANCE_URL,
|
SALESFORCE_INSTANCE_URL,
|
||||||
ZEN_BLACK_PRICE,
|
ZEN_BLACK_PRICE,
|
||||||
ZEN_LITE_PRICE,
|
ZEN_LITE_PRICE,
|
||||||
|
new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
|
||||||
new sst.Secret("ZEN_LIMITS"),
|
new sst.Secret("ZEN_LIMITS"),
|
||||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||||
...ZEN_MODELS,
|
...ZEN_MODELS,
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
const displayName = (s: string) =>
|
|
||||||
s
|
|
||||||
.split("-")
|
|
||||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
||||||
.join(" ")
|
|
||||||
.replace(/(?<=\d) (?=\d)/g, ".")
|
|
||||||
|
|
||||||
const resourceName = (s: string) => displayName(s).replace(/[^a-zA-Z0-9]/g, "")
|
|
||||||
|
|
||||||
const varSpec = (label: string, name: string) =>
|
|
||||||
$jsonStringify({
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
attrs: {
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
missing: false,
|
|
||||||
},
|
|
||||||
type: "varSpec",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
type: "paragraph",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
type: "doc",
|
|
||||||
})
|
|
||||||
|
|
||||||
const fields = {
|
|
||||||
model: incident.getAlertAttributeOutput({ name: "Model" }),
|
|
||||||
product: incident.getAlertAttributeOutput({ name: "Product" }),
|
|
||||||
}
|
|
||||||
|
|
||||||
const alertSource = new incident.AlertSource("HoneycombAlertSource", {
|
|
||||||
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
|
|
||||||
sourceType: "honeycomb",
|
|
||||||
template: {
|
|
||||||
title: {
|
|
||||||
literal: varSpec("Payload -> Title", "title"),
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
literal: varSpec("Payload -> Description", "description"),
|
|
||||||
},
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
alertAttributeId: fields.model.id,
|
|
||||||
binding: {
|
|
||||||
value: {
|
|
||||||
reference: 'expressions["model"]',
|
|
||||||
},
|
|
||||||
mergeStrategy: "first_wins",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alertAttributeId: fields.product.id,
|
|
||||||
binding: {
|
|
||||||
value: {
|
|
||||||
reference: 'expressions["product"]',
|
|
||||||
},
|
|
||||||
mergeStrategy: "first_wins",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
expressions: [
|
|
||||||
{
|
|
||||||
label: "Model",
|
|
||||||
operations: [
|
|
||||||
{
|
|
||||||
operationType: "parse",
|
|
||||||
parse: {
|
|
||||||
returns: {
|
|
||||||
array: false,
|
|
||||||
type: fields.model.type,
|
|
||||||
},
|
|
||||||
source: "$['model']",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
reference: "model",
|
|
||||||
rootReference: "payload",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Product",
|
|
||||||
operations: [
|
|
||||||
{
|
|
||||||
operationType: "parse",
|
|
||||||
parse: {
|
|
||||||
returns: {
|
|
||||||
array: false,
|
|
||||||
type: fields.product.type,
|
|
||||||
},
|
|
||||||
source: "$['product']",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
reference: "product",
|
|
||||||
rootReference: "payload",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const webhookRecipient = new honeycomb.WebhookRecipient(`IncidentWebhook`, {
|
|
||||||
name: $app.stage === "production" ? "Incident.io" : `Incident.io (${$app.stage})`,
|
|
||||||
url: alertSource.alertEventsUrl,
|
|
||||||
secret: alertSource.secretToken,
|
|
||||||
templates: [
|
|
||||||
{
|
|
||||||
type: "trigger",
|
|
||||||
body: $jsonStringify({
|
|
||||||
title: "{{ .Name }}",
|
|
||||||
description: "{{ .Description }}",
|
|
||||||
status: "{{ .Alert.Status }}",
|
|
||||||
deduplication_key: "{{ .Alert.InstanceID }}",
|
|
||||||
source_url: "{{ .Result.URL }}",
|
|
||||||
model: "{{ .Vars.model }}",
|
|
||||||
product: "{{ .Vars.product }}",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
variables: [
|
|
||||||
{
|
|
||||||
name: "model",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "product",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
new incident.AlertRoute("HoneycombAlertRoute", {
|
|
||||||
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
|
|
||||||
enabled: true,
|
|
||||||
isPrivate: false,
|
|
||||||
alertSources: [
|
|
||||||
{
|
|
||||||
alertSourceId: alertSource.id,
|
|
||||||
conditionGroups: [
|
|
||||||
{
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
subject: "alert.title",
|
|
||||||
operation: "is_set",
|
|
||||||
paramBindings: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
conditionGroups: [
|
|
||||||
{
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
subject: "alert.title",
|
|
||||||
operation: "is_set",
|
|
||||||
paramBindings: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
expressions: [],
|
|
||||||
escalationConfig: {
|
|
||||||
autoCancelEscalations: true,
|
|
||||||
escalationTargets: [],
|
|
||||||
},
|
|
||||||
incidentConfig: {
|
|
||||||
autoDeclineEnabled: true,
|
|
||||||
enabled: true,
|
|
||||||
conditionGroups: [],
|
|
||||||
deferTimeSeconds: 0,
|
|
||||||
groupingKeys: [
|
|
||||||
{
|
|
||||||
reference: $interpolate`alert.attributes.${fields.model.id}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reference: $interpolate`alert.attributes.${fields.product.id}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
groupingWindowSeconds: 3600,
|
|
||||||
},
|
|
||||||
incidentTemplate: {
|
|
||||||
name: {
|
|
||||||
value: {
|
|
||||||
literal: varSpec("Alert -> Title", "alert.title"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
value: {
|
|
||||||
literal: varSpec("Alert -> Description", "alert.description"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
startInTriage: {
|
|
||||||
value: {
|
|
||||||
literal: "true",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
severity: {
|
|
||||||
mergeStrategy: "first-wins",
|
|
||||||
},
|
|
||||||
incidentMode: {
|
|
||||||
value: {
|
|
||||||
literal: $app.stage === "production" ? "standard" : "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type Product = "go" | "zen"
|
|
||||||
|
|
||||||
type Trigger = (opts: { model: string; product: Product }) => {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
json: honeycomb.GetQuerySpecificationOutputArgs
|
|
||||||
threshold: { op: ">=" | "<="; value: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
type Model = { id: string; products: Product[]; triggers: Trigger[] }
|
|
||||||
|
|
||||||
const httpErrors: Trigger = ({ model, product }) => ({
|
|
||||||
id: "increased-http-errors",
|
|
||||||
title: `Increased HTTP Errors for ${displayName(model)} on ${displayName(product)}`,
|
|
||||||
description: `Detected increased rate of HTTP errors for ${displayName(model)} on OpenCode ${displayName(product)}`,
|
|
||||||
json: {
|
|
||||||
calculations: [
|
|
||||||
{
|
|
||||||
op: "COUNT",
|
|
||||||
name: "TOTAL",
|
|
||||||
filterCombination: "AND",
|
|
||||||
filters: [
|
|
||||||
{ column: "model", op: "=", value: model },
|
|
||||||
{ column: "event_type", op: "=", value: "completions" },
|
|
||||||
{ column: "user_agent", op: "contains", value: "opencode" },
|
|
||||||
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: "COUNT",
|
|
||||||
name: "FAILED",
|
|
||||||
filterCombination: "AND",
|
|
||||||
filters: [
|
|
||||||
{ column: "model", op: "=", value: model },
|
|
||||||
{ column: "event_type", op: "=", value: "completions" },
|
|
||||||
{ column: "user_agent", op: "contains", value: "opencode" },
|
|
||||||
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
|
|
||||||
{ column: "status", op: ">=", value: "400" },
|
|
||||||
{ column: "status", op: "!=", value: "401" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
formulas: [{ name: "ERROR", expression: "$FAILED / $TOTAL" }],
|
|
||||||
timeRange: 900,
|
|
||||||
},
|
|
||||||
threshold: { op: ">=", value: 0.8 },
|
|
||||||
})
|
|
||||||
|
|
||||||
const models: Model[] = [
|
|
||||||
{ id: "kimi-k2.6", products: ["go", "zen"], triggers: [httpErrors] },
|
|
||||||
{ id: "kimi-k2.5", products: ["go", "zen"], triggers: [httpErrors] },
|
|
||||||
{ id: "deepseek-v4-flash", products: ["go", "zen"], triggers: [httpErrors] },
|
|
||||||
{ id: "deepseek-v4-pro", products: ["go", "zen"], triggers: [httpErrors] },
|
|
||||||
{ id: "glm-5.1", products: ["go", "zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "glm-5", products: ["go"], triggers: [httpErrors] },
|
|
||||||
{ id: "qwen3.6-plus", products: ["go", "zen"], triggers: [httpErrors] },
|
|
||||||
{ id: "qwen3.5-plus", products: ["go"], triggers: [httpErrors] },
|
|
||||||
{ id: "minimax-m2.7", products: ["go", "zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "minimax-m2.5", products: ["go", "zen"], triggers: [httpErrors] },
|
|
||||||
{ id: "mimo-v2.5-pro", products: ["go"], triggers: [httpErrors] },
|
|
||||||
// { id: "mimo-v2.5", products: ["go"], triggers: [httpErrors] },
|
|
||||||
// { id: "mimo-v2-omni", products: ["go"], triggers: [httpErrors] },
|
|
||||||
// { id: "mimo-v2-pro", products: ["go"], triggers: [httpErrors] },
|
|
||||||
{ id: "claude-opus-4-7", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "claude-opus-4-6", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "claude-sonnet-4-6", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
{ id: "gpt-5.5", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
{ id: "big-pickle", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "minimax-m2.5-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "hy3-preview-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "nemotron-3-super-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "trinity-large-preview-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "ling-2.6-flash-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
]
|
|
||||||
|
|
||||||
if ($app.stage !== "production") {
|
|
||||||
models.splice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const model of models) {
|
|
||||||
for (const product of model.products) {
|
|
||||||
for (const trigger of model.triggers) {
|
|
||||||
const spec = trigger({ model: model.id, product })
|
|
||||||
|
|
||||||
new honeycomb.Trigger(resourceName(`${spec.id}-${product}-${model.id}`), {
|
|
||||||
name: spec.title,
|
|
||||||
description: spec.description,
|
|
||||||
queryJson: honeycomb.getQuerySpecificationOutput(spec.json).json,
|
|
||||||
alertType: "on_change",
|
|
||||||
frequency: 300,
|
|
||||||
thresholds: [{ ...spec.threshold, exceededLimit: 1 }],
|
|
||||||
recipients: [
|
|
||||||
{
|
|
||||||
id: webhookRecipient.id,
|
|
||||||
notificationDetails: [
|
|
||||||
{
|
|
||||||
variables: [
|
|
||||||
{ name: "model", value: model.id },
|
|
||||||
{ name: "product", value: product },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-cgqwEUyOYcOnh07Wz20qkPIrDeBaCBmKiis6HO1EAIU=",
|
"x86_64-linux": "sha256-tYAb5Mo39UW1VEejYuo0jW0jzH2OyY/HrqgiZL3rmjY=",
|
||||||
"aarch64-linux": "sha256-AVy0RQuuXiseIwJV9f9API8OEo1jcy84dVidkEXgnX8=",
|
"aarch64-linux": "sha256-3zGKV5UwokXpmY0nT1mry3IhNf2EQYLKT7ac+/trmQA=",
|
||||||
"aarch64-darwin": "sha256-RIS3/SuSXaMV9WcTXxOJWGDw96LcCFT6E8Ktc28/544=",
|
"aarch64-darwin": "sha256-oKXAut7eu/eW5a43OT8+aFuH1F1tuIldTs+7PUXSCv4=",
|
||||||
"x86_64-darwin": "sha256-GBwXIZQxy5F7tH9TJyWAonX5aETbZ/veAjeznDtsYmk="
|
"x86_64-darwin": "sha256-Az+9X1scOEhw3aOO8laKJoZjiuz3qlLTIk1bx25P/z4="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ stdenvNoCC.mkDerivation {
|
|||||||
--filter './packages/opencode' \
|
--filter './packages/opencode' \
|
||||||
--filter './packages/desktop' \
|
--filter './packages/desktop' \
|
||||||
--filter './packages/app' \
|
--filter './packages/app' \
|
||||||
|
--filter './packages/shared' \
|
||||||
--frozen-lockfile \
|
--frozen-lockfile \
|
||||||
--ignore-scripts \
|
--ignore-scripts \
|
||||||
--no-progress
|
--no-progress
|
||||||
|
|||||||
+17
-18
@@ -7,7 +7,6 @@
|
|||||||
sysctl,
|
sysctl,
|
||||||
makeBinaryWrapper,
|
makeBinaryWrapper,
|
||||||
models-dev,
|
models-dev,
|
||||||
ripgrep,
|
|
||||||
installShellFiles,
|
installShellFiles,
|
||||||
versionCheckHook,
|
versionCheckHook,
|
||||||
writableTmpDirAsHomeHook,
|
writableTmpDirAsHomeHook,
|
||||||
@@ -52,25 +51,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
runHook postBuild
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
|
|
||||||
installPhase = ''
|
installPhase =
|
||||||
runHook preInstall
|
''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
||||||
install -Dm644 schema.json $out/share/opencode/schema.json
|
install -Dm644 schema.json $out/share/opencode/schema.json
|
||||||
|
''
|
||||||
wrapProgram $out/bin/opencode \
|
# bun runs sysctl to detect if dunning on rosetta2
|
||||||
--prefix PATH : ${
|
+ lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
|
||||||
lib.makeBinPath (
|
wrapProgram $out/bin/opencode \
|
||||||
[
|
--prefix PATH : ${
|
||||||
ripgrep
|
lib.makeBinPath [
|
||||||
|
sysctl
|
||||||
]
|
]
|
||||||
# bun runs sysctl to detect if running on rosetta2
|
}
|
||||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
''
|
||||||
)
|
+ ''
|
||||||
}
|
runHook postInstall
|
||||||
|
'';
|
||||||
runHook postInstall
|
|
||||||
'';
|
|
||||||
|
|
||||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||||
# trick yargs into also generating zsh completions
|
# trick yargs into also generating zsh completions
|
||||||
|
|||||||
+8
-14
@@ -4,10 +4,10 @@
|
|||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.13",
|
"packageManager": "bun@1.3.11",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||||
"dev:web": "bun --cwd packages/app dev",
|
"dev:web": "bun --cwd packages/app dev",
|
||||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||||
@@ -27,34 +27,31 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@effect/opentelemetry": "4.0.0-beta.57",
|
"@effect/opentelemetry": "4.0.0-beta.48",
|
||||||
"@effect/platform-node": "4.0.0-beta.57",
|
"@effect/platform-node": "4.0.0-beta.48",
|
||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@types/bun": "1.3.12",
|
"@types/bun": "1.3.11",
|
||||||
"@types/cross-spawn": "6.0.6",
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.2.2",
|
|
||||||
"@opentui/solid": "0.2.2",
|
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "24.12.2",
|
"@types/node": "22.13.9",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@pierre/diffs": "1.1.0-beta.18",
|
"@pierre/diffs": "1.1.0-beta.18",
|
||||||
"opentui-spinner": "0.0.6",
|
|
||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.59",
|
"effect": "4.0.0-beta.48",
|
||||||
"ai": "6.0.168",
|
"ai": "6.0.158",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
@@ -77,8 +74,6 @@
|
|||||||
"@solidjs/meta": "0.29.4",
|
"@solidjs/meta": "0.29.4",
|
||||||
"@solidjs/router": "0.15.4",
|
"@solidjs/router": "0.15.4",
|
||||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||||
"@sentry/solid": "10.36.0",
|
|
||||||
"@sentry/vite-plugin": "4.6.0",
|
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
"vite-plugin-solid": "2.11.10",
|
"vite-plugin-solid": "2.11.10",
|
||||||
"@lydell/node-pty": "1.2.0-beta.10"
|
"@lydell/node-pty": "1.2.0-beta.10"
|
||||||
@@ -130,7 +125,6 @@
|
|||||||
"@types/node": "catalog:"
|
"@types/node": "catalog:"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.14.40",
|
"version": "1.4.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@happy-dom/global-registrator": "20.0.11",
|
"@happy-dom/global-registrator": "20.0.11",
|
||||||
"@playwright/test": "catalog:",
|
"@playwright/test": "catalog:",
|
||||||
"@sentry/vite-plugin": "catalog:",
|
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
@@ -41,10 +40,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@sentry/solid": "catalog:",
|
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/shared": "workspace:*",
|
||||||
"@shikijs/transformers": "3.9.2",
|
"@shikijs/transformers": "3.9.2",
|
||||||
"@solid-primitives/active-element": "2.1.3",
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
"@solid-primitives/audio": "1.4.2",
|
"@solid-primitives/audio": "1.4.2",
|
||||||
|
|||||||
Binary file not shown.
+16
-32
@@ -1,5 +1,4 @@
|
|||||||
import "@/index.css"
|
import "@/index.css"
|
||||||
import * as Sentry from "@sentry/solid"
|
|
||||||
import { I18nProvider } from "@opencode-ai/ui/context"
|
import { I18nProvider } from "@opencode-ai/ui/context"
|
||||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||||
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
||||||
@@ -83,15 +82,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function QueryProvider(props: ParentProps) {
|
function QueryProvider(props: ParentProps) {
|
||||||
const client = new QueryClient({
|
const client = new QueryClient()
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,12 +140,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
|||||||
>
|
>
|
||||||
<LanguageProvider locale={props.locale}>
|
<LanguageProvider locale={props.locale}>
|
||||||
<UiI18nBridge>
|
<UiI18nBridge>
|
||||||
<ErrorBoundary
|
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||||
fallback={(error) => {
|
|
||||||
Sentry.captureException(error)
|
|
||||||
return <ErrorPage error={error} />
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<DialogProvider>
|
<DialogProvider>
|
||||||
<MarkedProvider>
|
<MarkedProvider>
|
||||||
@@ -307,22 +293,20 @@ export function AppInterface(props: {
|
|||||||
>
|
>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<ServerKey>
|
<ServerKey>
|
||||||
<QueryProvider>
|
<GlobalSDKProvider>
|
||||||
<GlobalSDKProvider>
|
<GlobalSyncProvider>
|
||||||
<GlobalSyncProvider>
|
<Dynamic
|
||||||
<Dynamic
|
component={props.router ?? Router}
|
||||||
component={props.router ?? Router}
|
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
>
|
||||||
>
|
<Route path="/" component={HomeRoute} />
|
||||||
<Route path="/" component={HomeRoute} />
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
<Route path="/" component={SessionIndexRoute} />
|
||||||
<Route path="/" component={SessionIndexRoute} />
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
</Route>
|
||||||
</Route>
|
</Dynamic>
|
||||||
</Dynamic>
|
</GlobalSyncProvider>
|
||||||
</GlobalSyncProvider>
|
</GlobalSDKProvider>
|
||||||
</GlobalSDKProvider>
|
|
||||||
</QueryProvider>
|
|
||||||
</ServerKey>
|
</ServerKey>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
</ServerProvider>
|
</ServerProvider>
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ import { createStore } from "solid-js/store"
|
|||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getProjectAvatarSource } from "@/pages/layout/sidebar-items"
|
|
||||||
|
|
||||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||||
|
|
||||||
@@ -27,8 +26,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
name: defaultName(),
|
name: defaultName(),
|
||||||
color: props.project.icon?.color,
|
color: props.project.icon?.color || "pink",
|
||||||
iconOverride: props.project.icon?.override,
|
iconUrl: props.project.icon?.override || "",
|
||||||
startup: props.project.commands?.start ?? "",
|
startup: props.project.commands?.start ?? "",
|
||||||
dragOver: false,
|
dragOver: false,
|
||||||
iconHover: false,
|
iconHover: false,
|
||||||
@@ -40,7 +39,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
if (!file.type.startsWith("image/")) return
|
if (!file.type.startsWith("image/")) return
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
setStore("iconOverride", e.target?.result as string)
|
setStore("iconUrl", e.target?.result as string)
|
||||||
setStore("iconHover", false)
|
setStore("iconHover", false)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
@@ -69,7 +68,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearIcon() {
|
function clearIcon() {
|
||||||
setStore("iconOverride", "")
|
setStore("iconUrl", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveMutation = useMutation(() => ({
|
const saveMutation = useMutation(() => ({
|
||||||
@@ -82,17 +81,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
projectID: props.project.id,
|
projectID: props.project.id,
|
||||||
directory: props.project.worktree,
|
directory: props.project.worktree,
|
||||||
name,
|
name,
|
||||||
icon: { color: store.color || "", override: store.iconOverride || "" },
|
icon: { color: store.color, override: store.iconUrl },
|
||||||
commands: { start },
|
commands: { start },
|
||||||
})
|
})
|
||||||
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
||||||
dialog.close()
|
dialog.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
globalSync.project.meta(props.project.worktree, {
|
globalSync.project.meta(props.project.worktree, {
|
||||||
name,
|
name,
|
||||||
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||||
commands: { start: start || undefined },
|
commands: { start: start || undefined },
|
||||||
})
|
})
|
||||||
dialog.close()
|
dialog.close()
|
||||||
@@ -131,13 +130,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
classList={{
|
classList={{
|
||||||
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
||||||
"border-border-base hover:border-border-strong": !store.dragOver,
|
"border-border-base hover:border-border-strong": !store.dragOver,
|
||||||
"overflow-hidden": !!store.iconOverride,
|
"overflow-hidden": !!store.iconUrl,
|
||||||
}}
|
}}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (store.iconOverride && store.iconHover) {
|
if (store.iconUrl && store.iconHover) {
|
||||||
clearIcon()
|
clearIcon()
|
||||||
} else {
|
} else {
|
||||||
iconInput?.click()
|
iconInput?.click()
|
||||||
@@ -145,11 +144,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={getProjectAvatarSource(props.project.id, {
|
when={store.iconUrl}
|
||||||
color: store.color,
|
|
||||||
url: props.project.icon?.url,
|
|
||||||
override: store.iconOverride,
|
|
||||||
})}
|
|
||||||
fallback={
|
fallback={
|
||||||
<div class="size-full flex items-center justify-center">
|
<div class="size-full flex items-center justify-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -160,20 +155,18 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(src) => (
|
<img
|
||||||
<img
|
src={store.iconUrl}
|
||||||
src={src()}
|
alt={language.t("dialog.project.edit.icon.alt")}
|
||||||
alt={language.t("dialog.project.edit.icon.alt")}
|
class="size-full object-cover"
|
||||||
class="size-full object-cover"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !store.iconOverride,
|
"opacity-100": store.iconHover && !store.iconUrl,
|
||||||
"opacity-0": !(store.iconHover && !store.iconOverride),
|
"opacity-0": !(store.iconHover && !store.iconUrl),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
@@ -181,8 +174,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !!store.iconOverride,
|
"opacity-100": store.iconHover && !!store.iconUrl,
|
||||||
"opacity-0": !(store.iconHover && !!store.iconOverride),
|
"opacity-0": !(store.iconHover && !!store.iconUrl),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
@@ -205,7 +198,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!store.iconOverride}>
|
<Show when={!store.iconUrl}>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
@@ -222,10 +215,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||||
store.color !== color,
|
store.color !== color,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => setStore("color", color)}
|
||||||
if (store.color === color && !props.project.icon?.url) return
|
|
||||||
setStore("color", store.color === color ? undefined : color)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
fallback={store.name || defaultName()}
|
fallback={store.name || defaultName()}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { List } from "@opencode-ai/ui/list"
|
|||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
interface ForkableMessage {
|
interface ForkableMessage {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
|||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import type { ListRef } from "@opencode-ai/ui/list"
|
import type { ListRef } from "@opencode-ai/ui/list"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { createMemo, createResource, createSignal } from "solid-js"
|
import { createMemo, createResource, createSignal } from "solid-js"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { Component, createMemo, Show } from "solid-js"
|
import { Component, createEffect, createMemo, on, Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { mcpQueryKey } from "@/context/global-sync"
|
|
||||||
|
|
||||||
const statusLabels = {
|
const statusLabels = {
|
||||||
connected: "mcp.status.connected",
|
connected: "mcp.status.connected",
|
||||||
@@ -19,7 +20,48 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const queryClient = useQueryClient()
|
const [state, setState] = createStore({
|
||||||
|
done: false,
|
||||||
|
loading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => sync.data.mcp_ready,
|
||||||
|
(ready, prev) => {
|
||||||
|
if (!ready && prev) setState("done", false)
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (state.done || state.loading) return
|
||||||
|
if (sync.data.mcp_ready) {
|
||||||
|
setState("done", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setState("loading", true)
|
||||||
|
void sdk.client.mcp
|
||||||
|
.status()
|
||||||
|
.then((result) => {
|
||||||
|
sync.set("mcp", result.data ?? {})
|
||||||
|
sync.set("mcp_ready", true)
|
||||||
|
setState("done", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setState("done", true)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setState("loading", false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const items = createMemo(() =>
|
const items = createMemo(() =>
|
||||||
Object.entries(sync.data.mcp ?? {})
|
Object.entries(sync.data.mcp ?? {})
|
||||||
@@ -29,10 +71,16 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
|
|
||||||
const toggle = useMutation(() => ({
|
const toggle = useMutation(() => ({
|
||||||
mutationFn: async (name: string) => {
|
mutationFn: async (name: string) => {
|
||||||
if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
|
const status = sync.data.mcp[name]
|
||||||
else await sdk.client.mcp.connect({ name })
|
if (status?.status === "connected") {
|
||||||
|
await sdk.client.mcp.disconnect({ name })
|
||||||
|
} else {
|
||||||
|
await sdk.client.mcp.connect({ name })
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await sdk.client.mcp.status()
|
||||||
|
if (result.data) sync.set("mcp", result.data)
|
||||||
},
|
},
|
||||||
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||||
|
|||||||
@@ -504,7 +504,7 @@ export function DialogSelectServer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title={formTitle()}>
|
<Dialog title={formTitle()}>
|
||||||
<div class="flex flex-1 min-h-0 flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Show
|
<Show
|
||||||
when={!isFormMode()}
|
when={!isFormMode()}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -539,7 +539,7 @@ export function DialogSelectServer() {
|
|||||||
if (x) void select(x)
|
if (x) void select(x)
|
||||||
}}
|
}}
|
||||||
divider={true}
|
divider={true}
|
||||||
class="flex-1 min-h-0 px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
const key = ServerConnection.key(i)
|
const key = ServerConnection.key(i)
|
||||||
@@ -619,7 +619,7 @@ export function DialogSelectServer() {
|
|||||||
</List>
|
</List>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="shrink-0 px-5 pb-5">
|
<div class="px-5 pb-5">
|
||||||
<Show
|
<Show
|
||||||
when={isFormMode()}
|
when={isFormMode()}
|
||||||
fallback={
|
fallback={
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||||
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js"
|
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
} from "@/context/prompt"
|
} from "@/context/prompt"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
@@ -55,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
|||||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||||
import { useQueries } from "@tanstack/solid-query"
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
@@ -103,7 +102,6 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
|
|||||||
|
|
||||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const globalSDK = useGlobalSDK()
|
|
||||||
|
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
@@ -272,7 +270,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||||
const motion = (value: number) => ({
|
const motion = (value: number) => ({
|
||||||
opacity: value,
|
opacity: value,
|
||||||
transform: `scale(${0.98 + value * 0.02})`,
|
transform: `scale(${0.95 + value * 0.05})`,
|
||||||
filter: `blur(${(1 - value) * 2}px)`,
|
filter: `blur(${(1 - value) * 2}px)`,
|
||||||
"pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
|
"pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
|
||||||
})
|
})
|
||||||
@@ -347,7 +345,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
promptPlaceholder({
|
promptPlaceholder({
|
||||||
mode: store.mode,
|
mode: store.mode,
|
||||||
commentCount: commentCount(),
|
commentCount: commentCount(),
|
||||||
example: suggest() ? (store.mode === "shell" ? "git status" : language.t(EXAMPLES[store.placeholder])) : "",
|
example: suggest() ? language.t(EXAMPLES[store.placeholder]) : "",
|
||||||
suggest: suggest(),
|
suggest: suggest(),
|
||||||
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
|
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
|
||||||
}),
|
}),
|
||||||
@@ -1254,27 +1252,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
|
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||||
queries: [
|
|
||||||
loadAgentsQuery(sdk.directory, sdk.client),
|
|
||||||
loadProvidersQuery(null, globalSDK.client),
|
|
||||||
loadProvidersQuery(sdk.directory, sdk.client),
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
const agentsLoading = () => agentsQuery.isLoading
|
const agentsLoading = () => agentsQuery.isLoading
|
||||||
const agentsShouldFadeIn = createMemo((prev) => prev ?? agentsLoading())
|
|
||||||
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
|
||||||
const providersShouldFadeIn = createMemo((prev) => prev ?? providersLoading())
|
|
||||||
|
|
||||||
const [promptReady] = createResource(
|
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||||
() => prompt.ready().promise,
|
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||||
(p) => p,
|
|
||||||
)
|
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||||
{(promptReady(), null)}
|
|
||||||
<PromptPopover
|
<PromptPopover
|
||||||
popover={store.popover}
|
popover={store.popover}
|
||||||
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
|
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
|
||||||
@@ -1371,13 +1358,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
style={{ "padding-bottom": space }}
|
style={{ "padding-bottom": space }}
|
||||||
/>
|
/>
|
||||||
<div
|
<Show when={!prompt.dirty()}>
|
||||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
<div
|
||||||
classList={{ "font-mono!": store.mode === "shell" }}
|
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||||
style={{ "padding-bottom": space, display: prompt.dirty() ? "none" : undefined }}
|
classList={{ "font-mono!": store.mode === "shell" }}
|
||||||
>
|
style={{ "padding-bottom": space }}
|
||||||
{placeholder()}
|
>
|
||||||
</div>
|
{placeholder()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -1409,11 +1398,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
data-action="prompt-submit"
|
data-action="prompt-submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!working() && blank()}
|
disabled={store.mode !== "normal" || (!working() && blank())}
|
||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
|
icon={stopping() ? "stop" : "arrow-up"}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
class="size-8"
|
class="size-8"
|
||||||
|
style={buttons()}
|
||||||
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -1456,31 +1446,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
|
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
|
||||||
<div
|
<div
|
||||||
class="h-7 flex items-center gap-1.5 min-w-0 absolute inset-0"
|
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
|
||||||
style={{
|
style={{
|
||||||
padding: "0 0px 0 8px",
|
padding: "0 4px 0 8px",
|
||||||
...shell(),
|
...shell(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="console" />
|
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||||
<span class="truncate text-13-medium text-text-base">{language.t("prompt.mode.shell")}</span>
|
<div class="size-4 shrink-0" />
|
||||||
<div class="flex-1" />
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="text-text-base"
|
|
||||||
onClick={() => {
|
|
||||||
setStore("mode", "normal")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||||
<Show when={!agentsLoading()}>
|
<Show when={!agentsLoading()}>
|
||||||
<div
|
<div data-component="prompt-agent-control">
|
||||||
data-component="prompt-agent-control"
|
|
||||||
style={agentsShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
|
||||||
>
|
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
@@ -1506,10 +1483,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={!providersLoading()}>
|
<Show when={!providersLoading()}>
|
||||||
<Show when={store.mode !== "shell"}>
|
<Show when={store.mode !== "shell"}>
|
||||||
<div
|
<div data-component="prompt-model-control">
|
||||||
data-component="prompt-model-control"
|
|
||||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
|
||||||
>
|
|
||||||
<Show
|
<Show
|
||||||
when={providers.paid().length > 0}
|
when={providers.paid().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -1580,35 +1554,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={variants().length > 2}>
|
<div data-component="prompt-variant-control">
|
||||||
<div
|
<TooltipKeybind
|
||||||
data-component="prompt-variant-control"
|
placement="top"
|
||||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
gutter={4}
|
||||||
|
title={language.t("command.model.variant.cycle")}
|
||||||
|
keybind={command.keybind("model.variant.cycle")}
|
||||||
>
|
>
|
||||||
<TooltipKeybind
|
<Select
|
||||||
placement="top"
|
size="normal"
|
||||||
gutter={4}
|
options={variants()}
|
||||||
title={language.t("command.model.variant.cycle")}
|
current={local.model.variant.current() ?? "default"}
|
||||||
keybind={command.keybind("model.variant.cycle")}
|
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||||
>
|
onSelect={(value) => {
|
||||||
<Select
|
local.model.variant.set(value === "default" ? undefined : value)
|
||||||
size="normal"
|
restoreFocus()
|
||||||
options={variants()}
|
}}
|
||||||
current={local.model.variant.current() ?? "default"}
|
class="capitalize max-w-[160px] text-text-base"
|
||||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
valueClass="truncate text-13-regular text-text-base"
|
||||||
onSelect={(value) => {
|
triggerStyle={control()}
|
||||||
local.model.variant.set(value === "default" ? undefined : value)
|
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||||
restoreFocus()
|
variant="ghost"
|
||||||
}}
|
/>
|
||||||
class="capitalize max-w-[160px] text-text-base"
|
</TooltipKeybind>
|
||||||
valueClass="truncate text-13-regular text-text-base"
|
</div>
|
||||||
triggerStyle={control()}
|
|
||||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
|
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
import { encodeFilePath } from "@/context/file/path"
|
import { encodeFilePath } from "@/context/file/path"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Component, For, Show } from "solid-js"
|
|||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/shared/util/path"
|
||||||
import type { ContextItem } from "@/context/prompt"
|
import type { ContextItem } from "@/context/prompt"
|
||||||
|
|
||||||
type PromptContextItem = ContextItem & { key: string }
|
type PromptContextItem = ContextItem & { key: string }
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("promptPlaceholder", () => {
|
|||||||
suggest: true,
|
suggest: true,
|
||||||
t,
|
t,
|
||||||
})
|
})
|
||||||
expect(value).toBe("prompt.placeholder.shell:example")
|
expect(value).toBe("prompt.placeholder.shell")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns summarize placeholders for comment context", () => {
|
test("returns summarize placeholders for comment context", () => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type PromptPlaceholderInput = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function promptPlaceholder(input: PromptPlaceholderInput) {
|
export function promptPlaceholder(input: PromptPlaceholderInput) {
|
||||||
if (input.mode === "shell") return input.t("prompt.placeholder.shell", { example: input.example })
|
if (input.mode === "shell") return input.t("prompt.placeholder.shell")
|
||||||
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
|
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
|
||||||
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
|
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
|
||||||
if (!input.suggest) return input.t("prompt.placeholder.simple")
|
if (!input.suggest) return input.t("prompt.placeholder.simple")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, For, Match, Show, Switch } from "solid-js"
|
import { Component, For, Match, Show, Switch } from "solid-js"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
||||||
|
|
||||||
export type AtOption =
|
export type AtOption =
|
||||||
| { type: "agent"; name: string; display: string }
|
| { type: "agent"; name: string; display: string }
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ beforeAll(async () => {
|
|||||||
showToast: () => 0,
|
showToast: () => 0,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@opencode-ai/core/util/encode", () => ({
|
mock.module("@opencode-ai/shared/util/encode", () => ({
|
||||||
base64Encode: (value: string) => value,
|
base64Encode: (value: string) => value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Session } 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/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/shared/util/binary"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { batch, type Accessor } from "solid-js"
|
import { batch, type Accessor } from "solid-js"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/shared/util/encode"
|
||||||
import { findLast } from "@opencode-ai/core/util/array"
|
import { findLast } from "@opencode-ai/shared/util/array"
|
||||||
import { same } from "@/utils/same"
|
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"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { Keybind } from "@opencode-ai/ui/keybind"
|
|||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
import { createEffect, createMemo, For, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
@@ -16,7 +16,6 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { useSettings } from "@/context/settings"
|
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { focusTerminalById } from "@/pages/session/helpers"
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
@@ -135,7 +134,6 @@ export function SessionHeader() {
|
|||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const { params, view } = useSessionLayout()
|
const { params, view } = useSessionLayout()
|
||||||
@@ -153,11 +151,6 @@ export function SessionHeader() {
|
|||||||
})
|
})
|
||||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||||
const os = createMemo(() => detectOS(platform))
|
const os = createMemo(() => detectOS(platform))
|
||||||
const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"
|
|
||||||
const search = createMemo(() => !isDesktopBeta || settings.general.showSearch())
|
|
||||||
const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree())
|
|
||||||
const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal())
|
|
||||||
const status = createMemo(() => !isDesktopBeta || settings.general.showStatus())
|
|
||||||
|
|
||||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||||
finder: true,
|
finder: true,
|
||||||
@@ -269,16 +262,12 @@ export function SessionHeader() {
|
|||||||
.catch((err: unknown) => showRequestError(language, err))
|
.catch((err: unknown) => showRequestError(language, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null)
|
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||||
const [rightMount, setRightMount] = createSignal<HTMLElement | null>(null)
|
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
||||||
onMount(() => {
|
|
||||||
setCenterMount(document.getElementById("opencode-titlebar-center"))
|
|
||||||
setRightMount(document.getElementById("opencode-titlebar-right"))
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={search() && centerMount()}>
|
<Show when={centerMount()}>
|
||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount()}>
|
<Portal mount={mount()}>
|
||||||
<Button
|
<Button
|
||||||
@@ -426,28 +415,24 @@ export function SessionHeader() {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Show when={status()}>
|
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||||
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
<StatusPopover />
|
||||||
<StatusPopover />
|
</Tooltip>
|
||||||
</Tooltip>
|
<TooltipKeybind
|
||||||
</Show>
|
title={language.t("command.terminal.toggle")}
|
||||||
<Show when={term()}>
|
keybind={command.keybind("terminal.toggle")}
|
||||||
<TooltipKeybind
|
>
|
||||||
title={language.t("command.terminal.toggle")}
|
<Button
|
||||||
keybind={command.keybind("terminal.toggle")}
|
variant="ghost"
|
||||||
|
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
||||||
|
onClick={toggleTerminal}
|
||||||
|
aria-label={language.t("command.terminal.toggle")}
|
||||||
|
aria-expanded={view().terminal.opened()}
|
||||||
|
aria-controls="terminal-panel"
|
||||||
>
|
>
|
||||||
<Button
|
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
||||||
variant="ghost"
|
</Button>
|
||||||
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
</TooltipKeybind>
|
||||||
onClick={toggleTerminal}
|
|
||||||
aria-label={language.t("command.terminal.toggle")}
|
|
||||||
aria-expanded={view().terminal.opened()}
|
|
||||||
aria-controls="terminal-panel"
|
|
||||||
>
|
|
||||||
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div class="hidden md:flex items-center gap-1 shrink-0">
|
<div class="hidden md:flex items-center gap-1 shrink-0">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
@@ -466,32 +451,30 @@ export function SessionHeader() {
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
|
|
||||||
<Show when={tree()}>
|
<TooltipKeybind
|
||||||
<TooltipKeybind
|
title={language.t("command.fileTree.toggle")}
|
||||||
title={language.t("command.fileTree.toggle")}
|
keybind={command.keybind("fileTree.toggle")}
|
||||||
keybind={command.keybind("fileTree.toggle")}
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
|
onClick={() => layout.fileTree.toggle()}
|
||||||
|
aria-label={language.t("command.fileTree.toggle")}
|
||||||
|
aria-expanded={layout.fileTree.opened()}
|
||||||
|
aria-controls="file-tree-panel"
|
||||||
>
|
>
|
||||||
<Button
|
<div class="relative flex items-center justify-center size-4">
|
||||||
variant="ghost"
|
<Icon
|
||||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
size="small"
|
||||||
onClick={() => layout.fileTree.toggle()}
|
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
||||||
aria-label={language.t("command.fileTree.toggle")}
|
classList={{
|
||||||
aria-expanded={layout.fileTree.opened()}
|
"text-icon-strong": layout.fileTree.opened(),
|
||||||
aria-controls="file-tree-panel"
|
"text-icon-weak": !layout.fileTree.opened(),
|
||||||
>
|
}}
|
||||||
<div class="relative flex items-center justify-center size-4">
|
/>
|
||||||
<Icon
|
</div>
|
||||||
size="small"
|
</Button>
|
||||||
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
</TooltipKeybind>
|
||||||
classList={{
|
|
||||||
"text-icon-strong": layout.fileTree.opened(),
|
|
||||||
"text-icon-weak": !layout.fileTree.opened(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
||||||
|
|
||||||
const MAIN_WORKTREE = "main"
|
const MAIN_WORKTREE = "main"
|
||||||
const CREATE_WORKTREE = "create"
|
const CREATE_WORKTREE = "create"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
|||||||
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 { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import { showToast } from "@opencode-ai/ui/toast"
|
|||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
|
||||||
import {
|
import {
|
||||||
monoDefault,
|
monoDefault,
|
||||||
monoFontFamily,
|
monoFontFamily,
|
||||||
@@ -21,9 +19,6 @@ import {
|
|||||||
sansDefault,
|
sansDefault,
|
||||||
sansFontFamily,
|
sansFontFamily,
|
||||||
sansInput,
|
sansInput,
|
||||||
terminalDefault,
|
|
||||||
terminalFontFamily,
|
|
||||||
terminalInput,
|
|
||||||
useSettings,
|
useSettings,
|
||||||
} from "@/context/settings"
|
} from "@/context/settings"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
@@ -42,18 +37,6 @@ type ThemeOption = {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShellOption = {
|
|
||||||
path: string
|
|
||||||
name: string
|
|
||||||
acceptable: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShellSelectOption = {
|
|
||||||
id: string
|
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
||||||
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
||||||
const stopDemoSound = () => {
|
const stopDemoSound = () => {
|
||||||
@@ -89,6 +72,10 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
void theme.loadThemes()
|
||||||
|
})
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
checking: false,
|
checking: false,
|
||||||
})
|
})
|
||||||
@@ -119,7 +106,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
permission.disableAutoAccept(params.id, value)
|
permission.disableAutoAccept(params.id, value)
|
||||||
}
|
}
|
||||||
const desktop = createMemo(() => platform.platform === "desktop")
|
|
||||||
|
|
||||||
const check = () => {
|
const check = () => {
|
||||||
if (!platform.checkUpdate) return
|
if (!platform.checkUpdate) return
|
||||||
@@ -138,25 +124,27 @@ export const SettingsGeneral: Component = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = platform.updateAndRestart
|
const actions =
|
||||||
? [
|
platform.update && platform.restart
|
||||||
{
|
? [
|
||||||
label: language.t("toast.update.action.installRestart"),
|
{
|
||||||
onClick: async () => {
|
label: language.t("toast.update.action.installRestart"),
|
||||||
await platform.updateAndRestart!()
|
onClick: async () => {
|
||||||
|
await platform.update!()
|
||||||
|
await platform.restart!()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
label: language.t("toast.update.action.notYet"),
|
||||||
label: language.t("toast.update.action.notYet"),
|
onClick: "dismiss" as const,
|
||||||
onClick: "dismiss" as const,
|
},
|
||||||
},
|
]
|
||||||
]
|
: [
|
||||||
: [
|
{
|
||||||
{
|
label: language.t("toast.update.action.notYet"),
|
||||||
label: language.t("toast.update.action.notYet"),
|
onClick: "dismiss" as const,
|
||||||
onClick: "dismiss" as const,
|
},
|
||||||
},
|
]
|
||||||
]
|
|
||||||
|
|
||||||
showToast({
|
showToast({
|
||||||
persistent: true,
|
persistent: true,
|
||||||
@@ -175,70 +163,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||||
|
|
||||||
const globalSync = useGlobalSync()
|
|
||||||
const globalSdk = useGlobalSDK()
|
|
||||||
|
|
||||||
const [shells] = createResource(
|
|
||||||
() =>
|
|
||||||
globalSdk.client.pty
|
|
||||||
.shells()
|
|
||||||
.then((res) => res.data ?? [])
|
|
||||||
.catch(() => [] as ShellOption[]),
|
|
||||||
{ initialValue: [] as ShellOption[] },
|
|
||||||
)
|
|
||||||
|
|
||||||
const [displayBackend, { refetch: refetchDisplayBackend }] = createResource(
|
|
||||||
() => (linux() && platform.getDisplayBackend ? true : false),
|
|
||||||
() => Promise.resolve(platform.getDisplayBackend?.() ?? null).catch(() => null as DisplayBackend | null),
|
|
||||||
{ initialValue: null as DisplayBackend | null },
|
|
||||||
)
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
void theme.loadThemes()
|
|
||||||
})
|
|
||||||
|
|
||||||
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
|
|
||||||
const currentShell = createMemo(() => globalSync.data.config.shell ?? "")
|
|
||||||
|
|
||||||
const shellOptions = createMemo<ShellSelectOption[]>(() => {
|
|
||||||
const list = shells.latest
|
|
||||||
const current = globalSync.data.config.shell
|
|
||||||
|
|
||||||
const nameCounts = new Map<string, number>()
|
|
||||||
for (const s of list) {
|
|
||||||
nameCounts.set(s.name, (nameCounts.get(s.name) || 0) + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
autoOption,
|
|
||||||
...list.map((s) => {
|
|
||||||
const ambiguousName = (nameCounts.get(s.name) || 0) > 1
|
|
||||||
const text = ambiguousName ? s.path : s.name
|
|
||||||
const label = s.acceptable ? text : `${text} (${language.t("settings.general.row.shell.terminalOnly")})`
|
|
||||||
return {
|
|
||||||
id: s.path,
|
|
||||||
// Prefer name over path - "bash" is much cleaner than the explicit full route even when it may change due to PATH.
|
|
||||||
value: ambiguousName ? s.path : s.name,
|
|
||||||
label,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (current && !options.some((o) => o.value === current)) {
|
|
||||||
options.push({ id: current, value: current, label: current })
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
})
|
|
||||||
|
|
||||||
const onDisplayBackendChange = (checked: boolean) => {
|
|
||||||
const update = platform.setDisplayBackend?.(checked ? "wayland" : "auto")
|
|
||||||
if (!update) return
|
|
||||||
void update.finally(() => {
|
|
||||||
void refetchDisplayBackend()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
||||||
{ value: "system", label: language.t("theme.scheme.system") },
|
{ value: "system", label: language.t("theme.scheme.system") },
|
||||||
{ value: "light", label: language.t("theme.scheme.light") },
|
{ value: "light", label: language.t("theme.scheme.light") },
|
||||||
@@ -256,7 +180,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
||||||
const mono = () => monoInput(settings.appearance.font())
|
const mono = () => monoInput(settings.appearance.font())
|
||||||
const sans = () => sansInput(settings.appearance.uiFont())
|
const sans = () => sansInput(settings.appearance.uiFont())
|
||||||
const terminal = () => terminalInput(settings.appearance.terminalFont())
|
|
||||||
|
|
||||||
const soundSelectProps = (
|
const soundSelectProps = (
|
||||||
enabled: () => boolean,
|
enabled: () => boolean,
|
||||||
@@ -317,28 +240,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.shell.title")}
|
|
||||||
description={language.t("settings.general.row.shell.description")}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
data-action="settings-shell"
|
|
||||||
options={shellOptions()}
|
|
||||||
current={shellOptions().find((o) => o.value === currentShell()) ?? autoOption}
|
|
||||||
value={(o) => o.id}
|
|
||||||
label={(o) => o.label}
|
|
||||||
onSelect={(option) => {
|
|
||||||
if (!option) return
|
|
||||||
if (option.value === currentShell()) return
|
|
||||||
globalSync.updateConfig({ shell: option.value })
|
|
||||||
}}
|
|
||||||
variant="secondary"
|
|
||||||
size="small"
|
|
||||||
triggerVariant="settings"
|
|
||||||
triggerStyle={{ "min-width": "180px" }}
|
|
||||||
/>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.row.reasoningSummaries.title")}
|
title={language.t("settings.general.row.reasoningSummaries.title")}
|
||||||
description={language.t("settings.general.row.reasoningSummaries.description")}
|
description={language.t("settings.general.row.reasoningSummaries.description")}
|
||||||
@@ -374,86 +275,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.showSessionProgressBar.title")}
|
|
||||||
description={language.t("settings.general.row.showSessionProgressBar.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-session-progress-bar">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showSessionProgressBar()}
|
|
||||||
onChange={(checked) => settings.general.setShowSessionProgressBar(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</SettingsList>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const AdvancedSection = () => (
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.advanced")}</h3>
|
|
||||||
|
|
||||||
<SettingsList>
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.showFileTree.title")}
|
|
||||||
description={language.t("settings.general.row.showFileTree.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-file-tree">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showFileTree()}
|
|
||||||
onChange={(checked) => settings.general.setShowFileTree(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.showNavigation.title")}
|
|
||||||
description={language.t("settings.general.row.showNavigation.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-navigation">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showNavigation()}
|
|
||||||
onChange={(checked) => settings.general.setShowNavigation(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.showSearch.title")}
|
|
||||||
description={language.t("settings.general.row.showSearch.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-search">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showSearch()}
|
|
||||||
onChange={(checked) => settings.general.setShowSearch(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.showTerminal.title")}
|
|
||||||
description={language.t("settings.general.row.showTerminal.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-terminal">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showTerminal()}
|
|
||||||
onChange={(checked) => settings.general.setShowTerminal(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.showStatus.title")}
|
|
||||||
description={language.t("settings.general.row.showStatus.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-status">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showStatus()}
|
|
||||||
onChange={(checked) => settings.general.setShowStatus(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -561,29 +382,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.terminalFont.title")}
|
|
||||||
description={language.t("settings.general.row.terminalFont.description")}
|
|
||||||
>
|
|
||||||
<div class="w-full sm:w-[220px]">
|
|
||||||
<TextField
|
|
||||||
data-action="settings-terminal-font"
|
|
||||||
label={language.t("settings.general.row.terminalFont.title")}
|
|
||||||
hideLabel
|
|
||||||
type="text"
|
|
||||||
value={terminal()}
|
|
||||||
onChange={(value) => settings.appearance.setTerminalFont(value)}
|
|
||||||
placeholder={terminalDefault}
|
|
||||||
spellcheck={false}
|
|
||||||
autocorrect="off"
|
|
||||||
autocomplete="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
class="text-12-regular"
|
|
||||||
style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -729,7 +527,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(import.meta.env)
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
@@ -747,36 +544,70 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
<SoundsSection />
|
<SoundsSection />
|
||||||
|
|
||||||
|
{/*<Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled}>
|
||||||
|
{(_) => {
|
||||||
|
const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
|
||||||
|
const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.desktop.section.wsl")}</h3>
|
||||||
|
|
||||||
|
<SettingsList>
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.desktop.wsl.title")}
|
||||||
|
description={language.t("settings.desktop.wsl.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-wsl">
|
||||||
|
<Switch
|
||||||
|
checked={enabled() ?? false}
|
||||||
|
disabled={enabledResource.state === "pending"}
|
||||||
|
onChange={(checked) => platform.setWslEnabled?.(checked)?.finally(() => actions.refetch())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
</SettingsList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>*/}
|
||||||
|
|
||||||
<UpdatesSection />
|
<UpdatesSection />
|
||||||
|
|
||||||
<Show when={linux()}>
|
<Show when={linux()}>
|
||||||
<div class="flex flex-col gap-1">
|
{(_) => {
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
|
const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
|
||||||
|
const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest)
|
||||||
|
|
||||||
<SettingsList>
|
const onChange = (checked: boolean) =>
|
||||||
<SettingsRow
|
platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch())
|
||||||
title={
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>{language.t("settings.general.row.wayland.title")}</span>
|
|
||||||
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
|
|
||||||
<span class="text-text-weak">
|
|
||||||
<Icon name="help" size="small" />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
description={language.t("settings.general.row.wayland.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-wayland">
|
|
||||||
<Switch checked={displayBackend.latest === "wayland"} onChange={onDisplayBackendChange} />
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</SettingsList>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
|
return (
|
||||||
<AdvancedSection />
|
<div class="flex flex-col gap-1">
|
||||||
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
|
||||||
|
|
||||||
|
<SettingsList>
|
||||||
|
<SettingsRow
|
||||||
|
title={
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{language.t("settings.general.row.wayland.title")}</span>
|
||||||
|
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
|
||||||
|
<span class="text-text-weak">
|
||||||
|
<Icon name="help" size="small" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
description={language.t("settings.general.row.wayland.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-wayland">
|
||||||
|
<Switch checked={value() === "wayland"} onChange={onChange} />
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
</SettingsList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||||
@@ -15,7 +15,6 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||||
import { mcpQueryKey } from "@/context/global-sync"
|
|
||||||
|
|
||||||
const pollMs = 10_000
|
const pollMs = 10_000
|
||||||
|
|
||||||
@@ -138,14 +137,14 @@ const useMcpToggleMutation = () => {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation(() => ({
|
return useMutation(() => ({
|
||||||
mutationFn: async (name: string) => {
|
mutationFn: async (name: string) => {
|
||||||
const status = sync.data.mcp[name]
|
const status = sync.data.mcp[name]
|
||||||
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
||||||
|
const result = await sdk.client.mcp.status()
|
||||||
|
if (result.data) sync.set("mcp", result.data)
|
||||||
},
|
},
|
||||||
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
|
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -163,6 +162,14 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const sdk = useSDK()
|
||||||
|
|
||||||
|
const [load, setLoad] = createStore({
|
||||||
|
lspDone: false,
|
||||||
|
lspLoading: false,
|
||||||
|
mcpDone: false,
|
||||||
|
mcpLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
const fail = (err: unknown) => {
|
const fail = (err: unknown) => {
|
||||||
showToast({
|
showToast({
|
||||||
@@ -174,6 +181,40 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!props.shown()) return
|
if (!props.shown()) return
|
||||||
|
|
||||||
|
if (!sync.data.mcp_ready && !load.mcpDone && !load.mcpLoading) {
|
||||||
|
setLoad("mcpLoading", true)
|
||||||
|
void sdk.client.mcp
|
||||||
|
.status()
|
||||||
|
.then((result) => {
|
||||||
|
sync.set("mcp", result.data ?? {})
|
||||||
|
sync.set("mcp_ready", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoad("mcpDone", true)
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoad("mcpLoading", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sync.data.lsp_ready && !load.lspDone && !load.lspLoading) {
|
||||||
|
setLoad("lspLoading", true)
|
||||||
|
void sdk.client.lsp
|
||||||
|
.status()
|
||||||
|
.then((result) => {
|
||||||
|
sync.set("lsp", result.data ?? [])
|
||||||
|
sync.set("lsp_ready", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoad("lspDone", true)
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoad("lspLoading", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let dialogRun = 0
|
let dialogRun = 0
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ 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 { useServer } from "@/context/server"
|
||||||
import { terminalFontFamily, useSettings } from "@/context/settings"
|
import { monoFontFamily, useSettings } from "@/context/settings"
|
||||||
import type { LocalPTY } from "@/context/terminal"
|
import type { LocalPTY } from "@/context/terminal"
|
||||||
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"
|
||||||
import { terminalWebSocketURL } from "@/utils/terminal-websocket-url"
|
|
||||||
|
|
||||||
const TOGGLE_TERMINAL_ID = "terminal.toggle"
|
const TOGGLE_TERMINAL_ID = "terminal.toggle"
|
||||||
const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
|
const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
|
||||||
@@ -68,6 +67,13 @@ const debugTerminal = (...values: unknown[]) => {
|
|||||||
console.debug("[terminal]", ...values)
|
console.debug("[terminal]", ...values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errorName = (err: unknown) => {
|
||||||
|
if (!err || typeof err !== "object") return
|
||||||
|
if (!("name" in err)) return
|
||||||
|
const errorName = err.name
|
||||||
|
return typeof errorName === "string" ? errorName : undefined
|
||||||
|
}
|
||||||
|
|
||||||
const useTerminalUiBindings = (input: {
|
const useTerminalUiBindings = (input: {
|
||||||
container: HTMLDivElement
|
container: HTMLDivElement
|
||||||
term: Term
|
term: Term
|
||||||
@@ -294,7 +300,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const font = terminalFontFamily(settings.appearance.terminalFont())
|
const font = monoFontFamily(settings.appearance.font())
|
||||||
if (!term) return
|
if (!term) return
|
||||||
setOptionIfSupported(term, "fontFamily", font)
|
setOptionIfSupported(term, "fontFamily", font)
|
||||||
scheduleFit()
|
scheduleFit()
|
||||||
@@ -354,7 +360,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
cols: restoreSize?.cols,
|
cols: restoreSize?.cols,
|
||||||
rows: restoreSize?.rows,
|
rows: restoreSize?.rows,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: terminalFontFamily(settings.appearance.terminalFont()),
|
fontFamily: monoFontFamily(settings.appearance.font()),
|
||||||
allowTransparency: false,
|
allowTransparency: false,
|
||||||
convertEol: false,
|
convertEol: false,
|
||||||
theme: terminalColors(),
|
theme: terminalColors(),
|
||||||
@@ -472,34 +478,14 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
|
|
||||||
const gone = () =>
|
const gone = () =>
|
||||||
client.pty
|
client.pty
|
||||||
.get({ ptyID: id }, { throwOnError: false })
|
.get({ ptyID: id })
|
||||||
.then((result) => result.response.status === 404)
|
.then(() => false)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
if (errorName(err) === "NotFoundError") return true
|
||||||
debugTerminal("failed to inspect terminal session", err)
|
debugTerminal("failed to inspect terminal session", err)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const connectToken = async () => {
|
|
||||||
const result = await client.pty
|
|
||||||
.connectToken(
|
|
||||||
{ ptyID: id, directory },
|
|
||||||
{
|
|
||||||
throwOnError: false,
|
|
||||||
headers: { "x-opencode-ticket": "1" },
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
if (err instanceof Error && err.message.includes("Request is not supported")) return
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
if (!result) return
|
|
||||||
if (result.response.status === 200 && result.data?.ticket) return result.data.ticket
|
|
||||||
if (result.response.status === 404 || result.response.status === 405) return
|
|
||||||
if (result.response.status === 403)
|
|
||||||
throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
|
|
||||||
throw new Error(`PTY connect ticket failed with ${result.response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const retry = (err: unknown) => {
|
const retry = (err: unknown) => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
if (reconn !== undefined) return
|
if (reconn !== undefined) return
|
||||||
@@ -519,30 +505,22 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}, ms)
|
}, ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
const open = async () => {
|
const open = () => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
drop?.()
|
drop?.()
|
||||||
|
|
||||||
const ticket = await connectToken().catch((err) => {
|
const next = new URL(url + `/pty/${id}/connect`)
|
||||||
fail(err)
|
next.searchParams.set("directory", directory)
|
||||||
return undefined
|
next.searchParams.set("cursor", String(seek))
|
||||||
})
|
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
||||||
if (once.value) return
|
if (!sameOrigin && password) {
|
||||||
if (disposed) return
|
next.searchParams.set("auth_token", btoa(`${username}:${password}`))
|
||||||
|
// For same-origin requests, let the browser reuse the page's existing auth.
|
||||||
|
next.username = username
|
||||||
|
next.password = password
|
||||||
|
}
|
||||||
|
|
||||||
const socket = new WebSocket(
|
const socket = new WebSocket(next)
|
||||||
terminalWebSocketURL({
|
|
||||||
url,
|
|
||||||
id,
|
|
||||||
directory,
|
|
||||||
cursor: seek,
|
|
||||||
ticket,
|
|
||||||
sameOrigin,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
authToken: server.current?.type === "http" ? server.current.authToken : false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
socket.binaryType = "arraybuffer"
|
socket.binaryType = "arraybuffer"
|
||||||
ws = socket
|
ws = socket
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useLayout } from "@/context/layout"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
|
||||||
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
||||||
|
|
||||||
type TauriDesktopWindow = {
|
type TauriDesktopWindow = {
|
||||||
@@ -35,16 +34,12 @@ type TauriApi = {
|
|||||||
const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
|
const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
|
||||||
const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
|
const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
|
||||||
const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
|
const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
|
||||||
const titlebarHeight = 40
|
|
||||||
const minTitlebarZoom = 0.25
|
|
||||||
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
|
|
||||||
|
|
||||||
export function Titlebar() {
|
export function Titlebar() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -54,14 +49,7 @@ export function Titlebar() {
|
|||||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||||
const web = createMemo(() => platform.platform === "web")
|
const web = createMemo(() => platform.platform === "web")
|
||||||
const zoom = () => platform.webviewZoom?.() ?? 1
|
const zoom = () => platform.webviewZoom?.() ?? 1
|
||||||
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined)
|
||||||
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
|
|
||||||
const minHeight = () => {
|
|
||||||
if (mac()) return `${titlebarHeight / zoom()}px`
|
|
||||||
if (windows()) return `${titlebarHeight / Math.min(titlebarZoom(), 1)}px`
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const windowsControlsWidth = () => `${windowsControlsBaseWidth / Math.max(titlebarZoom(), 1)}px`
|
|
||||||
|
|
||||||
const [history, setHistory] = createStore({
|
const [history, setHistory] = createStore({
|
||||||
stack: [] as string[],
|
stack: [] as string[],
|
||||||
@@ -90,7 +78,6 @@ export function Titlebar() {
|
|||||||
const canBack = createMemo(() => history.index > 0)
|
const canBack = createMemo(() => history.index > 0)
|
||||||
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
||||||
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||||
const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation())
|
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
const next = backPath(history)
|
const next = backPath(history)
|
||||||
@@ -175,161 +162,157 @@ export function Titlebar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
class="h-10 shrink-0 bg-background-base relative overflow-hidden"
|
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
||||||
style={{ "min-height": minHeight() }}
|
style={{ "min-height": minHeight() }}
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
onDblClick={maximize}
|
onDblClick={maximize}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="grid h-full min-h-full w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
classList={{
|
||||||
style={{ zoom: counterZoom() }}
|
"flex items-center min-w-0": true,
|
||||||
|
"pl-2": !mac(),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<Show when={mac()}>
|
||||||
classList={{
|
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
|
||||||
"flex items-center min-w-0": true,
|
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||||
"pl-2": !mac(),
|
<IconButton
|
||||||
}}
|
icon="menu"
|
||||||
>
|
variant="ghost"
|
||||||
<Show when={mac()}>
|
class="titlebar-icon rounded-md"
|
||||||
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
|
onClick={layout.mobileSidebar.toggle}
|
||||||
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
aria-label={language.t("sidebar.menu.toggle")}
|
||||||
<IconButton
|
aria-expanded={layout.mobileSidebar.opened()}
|
||||||
icon="menu"
|
/>
|
||||||
variant="ghost"
|
</div>
|
||||||
class="titlebar-icon rounded-md"
|
</Show>
|
||||||
onClick={layout.mobileSidebar.toggle}
|
<Show when={!mac()}>
|
||||||
aria-label={language.t("sidebar.menu.toggle")}
|
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
||||||
aria-expanded={layout.mobileSidebar.opened()}
|
<IconButton
|
||||||
/>
|
icon="menu"
|
||||||
</div>
|
variant="ghost"
|
||||||
</Show>
|
class="titlebar-icon rounded-md"
|
||||||
<Show when={!mac()}>
|
onClick={layout.mobileSidebar.toggle}
|
||||||
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
aria-label={language.t("sidebar.menu.toggle")}
|
||||||
<IconButton
|
aria-expanded={layout.mobileSidebar.opened()}
|
||||||
icon="menu"
|
/>
|
||||||
variant="ghost"
|
</div>
|
||||||
class="titlebar-icon rounded-md"
|
</Show>
|
||||||
onClick={layout.mobileSidebar.toggle}
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
aria-label={language.t("sidebar.menu.toggle")}
|
<TooltipKeybind
|
||||||
aria-expanded={layout.mobileSidebar.opened()}
|
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||||
/>
|
placement="bottom"
|
||||||
</div>
|
title={language.t("command.sidebar.toggle")}
|
||||||
</Show>
|
keybind={command.keybind("sidebar.toggle")}
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
>
|
||||||
<TooltipKeybind
|
<Button
|
||||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
variant="ghost"
|
||||||
placement="bottom"
|
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
title={language.t("command.sidebar.toggle")}
|
onClick={layout.sidebar.toggle}
|
||||||
keybind={command.keybind("sidebar.toggle")}
|
aria-label={language.t("command.sidebar.toggle")}
|
||||||
|
aria-expanded={layout.sidebar.opened()}
|
||||||
>
|
>
|
||||||
<Button
|
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
||||||
variant="ghost"
|
</Button>
|
||||||
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
</TooltipKeybind>
|
||||||
onClick={layout.sidebar.toggle}
|
<div class="hidden xl:flex items-center shrink-0">
|
||||||
aria-label={language.t("command.sidebar.toggle")}
|
<Show when={params.dir}>
|
||||||
aria-expanded={layout.sidebar.opened()}
|
<div
|
||||||
|
class="flex items-center shrink-0 w-8 mr-1"
|
||||||
|
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
||||||
>
|
>
|
||||||
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
<div class="hidden xl:flex items-center shrink-0">
|
|
||||||
<Show when={params.dir}>
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center shrink-0 w-8 mr-1"
|
class="transition-opacity"
|
||||||
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
classList={{
|
||||||
|
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
||||||
|
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<TooltipKeybind
|
||||||
class="transition-opacity"
|
placement="bottom"
|
||||||
classList={{
|
title={language.t("command.session.new")}
|
||||||
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
keybind={command.keybind("session.new")}
|
||||||
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
openDelay={2000}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TooltipKeybind
|
<Button
|
||||||
placement="bottom"
|
variant="ghost"
|
||||||
title={language.t("command.session.new")}
|
icon={creating() ? "new-session-active" : "new-session"}
|
||||||
keybind={command.keybind("session.new")}
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
openDelay={2000}
|
disabled={layout.sidebar.opened()}
|
||||||
>
|
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
||||||
<Button
|
onClick={() => {
|
||||||
variant="ghost"
|
if (!params.dir) return
|
||||||
icon={creating() ? "new-session-active" : "new-session"}
|
navigate(`/${params.dir}/session`)
|
||||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
}}
|
||||||
disabled={layout.sidebar.opened()}
|
aria-label={language.t("command.session.new")}
|
||||||
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
aria-current={creating() ? "page" : undefined}
|
||||||
onClick={() => {
|
/>
|
||||||
if (!params.dir) return
|
</TooltipKeybind>
|
||||||
navigate(`/${params.dir}/session`)
|
</div>
|
||||||
}}
|
</div>
|
||||||
aria-label={language.t("command.session.new")}
|
</Show>
|
||||||
aria-current={creating() ? "page" : undefined}
|
<div
|
||||||
/>
|
class="flex items-center shrink-0"
|
||||||
</TooltipKeybind>
|
classList={{
|
||||||
</div>
|
"translate-x-0": !layout.sidebar.opened(),
|
||||||
|
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||||
|
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||||
|
"duration-180 ease-in": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={hasProjects()}>
|
||||||
|
<div class="flex items-center gap-0 transition-transform">
|
||||||
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="chevron-left"
|
||||||
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
|
disabled={!canBack()}
|
||||||
|
onClick={back}
|
||||||
|
aria-label={language.t("common.goBack")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="chevron-right"
|
||||||
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
|
disabled={!canForward()}
|
||||||
|
onClick={forward}
|
||||||
|
aria-label={language.t("common.goForward")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div
|
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||||
class="flex items-center shrink-0"
|
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
||||||
classList={{
|
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||||
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
|
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
||||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
</div>
|
||||||
"duration-180 ease-in": layout.sidebar.opened(),
|
)}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={hasProjects() && nav()}>
|
|
||||||
<div class="flex items-center gap-0 transition-transform">
|
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
icon="chevron-left"
|
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
|
||||||
disabled={!canBack()}
|
|
||||||
onClick={back}
|
|
||||||
aria-label={language.t("common.goBack")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
icon="chevron-right"
|
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
|
||||||
disabled={!canForward()}
|
|
||||||
onClick={forward}
|
|
||||||
aria-label={language.t("common.goForward")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
|
||||||
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
|
||||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
|
||||||
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||||
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
|
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"flex items-center min-w-0 justify-end": true,
|
"flex items-center min-w-0 justify-end": true,
|
||||||
"pr-2": !windows(),
|
"pr-2": !windows(),
|
||||||
}}
|
}}
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
>
|
>
|
||||||
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
||||||
<Show when={windows()}>
|
<Show when={windows()}>
|
||||||
{!tauriApi() && <div class="shrink-0" style={{ width: windowsControlsWidth() }} />}
|
{!tauriApi() && <div class="w-36 shrink-0" />}
|
||||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|||||||
@@ -8,31 +8,25 @@ import type {
|
|||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import type { InitError } from "../pages/error"
|
import type { InitError } from "../pages/error"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import {
|
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
|
||||||
bootstrapDirectory,
|
|
||||||
bootstrapGlobal,
|
|
||||||
clearProviderRev,
|
|
||||||
loadGlobalConfigQuery,
|
|
||||||
loadPathQuery,
|
|
||||||
loadProvidersQuery,
|
|
||||||
} from "./global-sync/bootstrap"
|
|
||||||
import { createChildStoreManager } from "./global-sync/child-store"
|
import { createChildStoreManager } from "./global-sync/child-store"
|
||||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
||||||
|
import { createRefreshQueue } from "./global-sync/queue"
|
||||||
import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
|
import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
|
||||||
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
||||||
import { trimSessions } from "./global-sync/session-trim"
|
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 { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
|
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||||
import { createRefreshQueue } from "./global-sync/queue"
|
|
||||||
import { directoryKey } from "./global-sync/utils"
|
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -48,23 +42,8 @@ type GlobalStore = {
|
|||||||
reload: undefined | "pending" | "complete"
|
reload: undefined | "pending" | "complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const
|
export const loadSessionsQuery = (directory: string) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
||||||
export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const
|
|
||||||
|
|
||||||
export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: mcpQueryKey(directory),
|
|
||||||
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const lspQueryKey = (directory: string) => [directory, "lsp"] as const
|
|
||||||
|
|
||||||
export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: lspQueryKey(directory),
|
|
||||||
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
|
|
||||||
})
|
|
||||||
|
|
||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
@@ -77,53 +56,54 @@ function createGlobalSync() {
|
|||||||
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 }>()
|
||||||
|
|
||||||
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
|
const [projectCache, setProjectCache, projectInit] = persisted(
|
||||||
queries: [
|
Persist.global("globalSync.project", ["globalSync.project.v1"]),
|
||||||
loadGlobalConfigQuery(globalSDK.client),
|
createStore({ value: [] as Project[] }),
|
||||||
loadProvidersQuery(null, globalSDK.client),
|
)
|
||||||
loadPathQuery(null, globalSDK.client),
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
||||||
get ready() {
|
ready: false,
|
||||||
return bootstrap.isPending
|
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||||
},
|
project: projectCache.value,
|
||||||
project: [],
|
|
||||||
session_todo: {},
|
session_todo: {},
|
||||||
|
provider: { all: [], connected: [], default: {} },
|
||||||
provider_auth: {},
|
provider_auth: {},
|
||||||
get path() {
|
config: {},
|
||||||
const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" }
|
reload: undefined,
|
||||||
if (pathQuery.isLoading) return EMPTY
|
|
||||||
return pathQuery.data ?? EMPTY
|
|
||||||
},
|
|
||||||
get provider() {
|
|
||||||
const EMPTY = { all: [], connected: [], default: {} }
|
|
||||||
if (providerQuery.isLoading) return EMPTY
|
|
||||||
return providerQuery.data ?? EMPTY
|
|
||||||
},
|
|
||||||
get config() {
|
|
||||||
if (configQuery.isLoading) return {}
|
|
||||||
return configQuery.data ?? {}
|
|
||||||
},
|
|
||||||
get reload() {
|
|
||||||
return updateConfigMutation.isPending ? "pending" : undefined
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
let active = true
|
||||||
|
let projectWritten = false
|
||||||
let bootedAt = 0
|
let bootedAt = 0
|
||||||
let bootingRoot = false
|
let bootingRoot = false
|
||||||
let eventFrame: number | undefined
|
let eventFrame: number | undefined
|
||||||
let eventTimer: ReturnType<typeof setTimeout> | undefined
|
let eventTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
active = false
|
||||||
|
})
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
||||||
if (eventTimer !== undefined) clearTimeout(eventTimer)
|
if (eventTimer !== undefined) clearTimeout(eventTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => {
|
const cacheProjects = () => {
|
||||||
|
setProjectCache(
|
||||||
|
"value",
|
||||||
|
untrack(() => globalStore.project.map(sanitizeProject)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setProjects = (next: Project[] | ((draft: Project[]) => void)) => {
|
||||||
|
projectWritten = true
|
||||||
|
if (typeof next === "function") {
|
||||||
|
setGlobalStore("project", produce(next))
|
||||||
|
cacheProjects()
|
||||||
|
return
|
||||||
|
}
|
||||||
setGlobalStore("project", next)
|
setGlobalStore("project", next)
|
||||||
|
cacheProjects()
|
||||||
}
|
}
|
||||||
|
|
||||||
const setBootStore = ((...input: unknown[]) => {
|
const setBootStore = ((...input: unknown[]) => {
|
||||||
@@ -134,30 +114,24 @@ function createGlobalSync() {
|
|||||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||||
}) as typeof setGlobalStore
|
}) as typeof setGlobalStore
|
||||||
|
|
||||||
const bootstrap = useQuery(() => ({
|
|
||||||
queryKey: ["bootstrap"],
|
|
||||||
queryFn: async () => {
|
|
||||||
await bootstrapGlobal({
|
|
||||||
globalSDK: globalSDK.client,
|
|
||||||
requestFailedTitle: language.t("common.requestFailed"),
|
|
||||||
translate: language.t,
|
|
||||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
|
||||||
setGlobalStore: setBootStore,
|
|
||||||
queryClient,
|
|
||||||
})
|
|
||||||
bootedAt = Date.now()
|
|
||||||
return bootedAt
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const set = ((...input: unknown[]) => {
|
const set = ((...input: unknown[]) => {
|
||||||
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
||||||
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
|
setProjects(input[1] as Project[] | ((draft: Project[]) => void))
|
||||||
return input[1]
|
return input[1]
|
||||||
}
|
}
|
||||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||||
}) as typeof setGlobalStore
|
}) as typeof setGlobalStore
|
||||||
|
|
||||||
|
if (projectInit instanceof Promise) {
|
||||||
|
void projectInit.then(() => {
|
||||||
|
if (!active) return
|
||||||
|
if (projectWritten) return
|
||||||
|
const cached = projectCache.value
|
||||||
|
if (cached.length === 0) return
|
||||||
|
setGlobalStore("project", cached)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
|
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
|
||||||
if (!sessionID) return
|
if (!sessionID) return
|
||||||
if (!todos) {
|
if (!todos) {
|
||||||
@@ -176,23 +150,10 @@ function createGlobalSync() {
|
|||||||
|
|
||||||
const queue = createRefreshQueue({
|
const queue = createRefreshQueue({
|
||||||
paused,
|
paused,
|
||||||
key: directoryKey,
|
bootstrap,
|
||||||
bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }),
|
|
||||||
bootstrapInstance,
|
bootstrapInstance,
|
||||||
})
|
})
|
||||||
|
|
||||||
const sdkFor = (directory: string) => {
|
|
||||||
const key = directoryKey(directory)
|
|
||||||
const cached = sdkCache.get(key)
|
|
||||||
if (cached) return cached
|
|
||||||
const sdk = globalSDK.createClient({
|
|
||||||
directory,
|
|
||||||
throwOnError: true,
|
|
||||||
})
|
|
||||||
sdkCache.set(key, sdk)
|
|
||||||
return sdk
|
|
||||||
}
|
|
||||||
|
|
||||||
const children = createChildStoreManager({
|
const children = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
isBooting: (directory) => booting.has(directory),
|
isBooting: (directory) => booting.has(directory),
|
||||||
@@ -201,28 +162,33 @@ function createGlobalSync() {
|
|||||||
void bootstrapInstance(directory)
|
void bootstrapInstance(directory)
|
||||||
},
|
},
|
||||||
onDispose: (directory) => {
|
onDispose: (directory) => {
|
||||||
const key = directoryKey(directory)
|
queue.clear(directory)
|
||||||
queue.clear(key)
|
sessionMeta.delete(directory)
|
||||||
sessionMeta.delete(key)
|
sdkCache.delete(directory)
|
||||||
sdkCache.delete(key)
|
clearProviderRev(directory)
|
||||||
clearProviderRev(key)
|
clearSessionPrefetchDirectory(directory)
|
||||||
clearSessionPrefetchDirectory(key)
|
|
||||||
},
|
},
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
getSdk: sdkFor,
|
|
||||||
global: {
|
|
||||||
provider: globalStore.provider,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sdkFor = (directory: string) => {
|
||||||
|
const cached = sdkCache.get(directory)
|
||||||
|
if (cached) return cached
|
||||||
|
const sdk = globalSDK.createClient({
|
||||||
|
directory,
|
||||||
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
sdkCache.set(directory, sdk)
|
||||||
|
return sdk
|
||||||
|
}
|
||||||
|
|
||||||
async function loadSessions(directory: string) {
|
async function loadSessions(directory: string) {
|
||||||
const key = directoryKey(directory)
|
const pending = sessionLoads.get(directory)
|
||||||
const pending = sessionLoads.get(key)
|
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
children.pin(key)
|
children.pin(directory)
|
||||||
const [store, setStore] = children.child(directory, { bootstrap: false })
|
const [store, setStore] = children.child(directory, { bootstrap: false })
|
||||||
const meta = sessionMeta.get(key)
|
const meta = sessionMeta.get(directory)
|
||||||
if (meta && meta.limit >= store.limit) {
|
if (meta && meta.limit >= store.limit) {
|
||||||
const next = trimSessions(store.session, {
|
const next = trimSessions(store.session, {
|
||||||
limit: store.limit,
|
limit: store.limit,
|
||||||
@@ -232,14 +198,14 @@ function createGlobalSync() {
|
|||||||
setStore("session", reconcile(next, { key: "id" }))
|
setStore("session", reconcile(next, { key: "id" }))
|
||||||
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
|
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
|
||||||
}
|
}
|
||||||
children.unpin(key)
|
children.unpin(directory)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||||
const promise = queryClient
|
const promise = queryClient
|
||||||
.fetchQuery({
|
.ensureQueryData({
|
||||||
queryKey: loadSessionsQueryKey(key),
|
...loadSessionsQuery(directory),
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
loadRootSessionsWithFallback({
|
loadRootSessionsWithFallback({
|
||||||
directory,
|
directory,
|
||||||
@@ -257,19 +223,17 @@ function createGlobalSync() {
|
|||||||
limit,
|
limit,
|
||||||
permission: store.permission,
|
permission: store.permission,
|
||||||
})
|
})
|
||||||
batch(() => {
|
setStore(
|
||||||
setStore(
|
"sessionTotal",
|
||||||
"sessionTotal",
|
estimateRootSessionTotal({
|
||||||
estimateRootSessionTotal({
|
count: nonArchived.length,
|
||||||
count: nonArchived.length,
|
limit: x.limit,
|
||||||
limit: x.limit,
|
limited: x.limited,
|
||||||
limited: x.limited,
|
}),
|
||||||
}),
|
)
|
||||||
)
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
sessionMeta.set(directory, { limit })
|
||||||
})
|
|
||||||
sessionMeta.set(key, { limit })
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Failed to load sessions", err)
|
console.error("Failed to load sessions", err)
|
||||||
@@ -284,24 +248,24 @@ function createGlobalSync() {
|
|||||||
})
|
})
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
|
|
||||||
sessionLoads.set(key, promise)
|
sessionLoads.set(directory, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
sessionLoads.delete(key)
|
sessionLoads.delete(directory)
|
||||||
children.unpin(key)
|
children.unpin(directory)
|
||||||
})
|
})
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrapInstance(directory: string) {
|
async function bootstrapInstance(directory: string) {
|
||||||
const key = directoryKey(directory)
|
if (!directory) return
|
||||||
if (!key) return
|
const pending = booting.get(directory)
|
||||||
const pending = booting.get(key)
|
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
children.pin(key)
|
children.pin(directory)
|
||||||
const promise = Promise.resolve().then(async () => {
|
const promise = Promise.resolve().then(async () => {
|
||||||
const child = children.ensureChild(directory)
|
const child = children.ensureChild(directory)
|
||||||
const cache = children.vcsCache.get(key)
|
child[1]("bootstrapPromise", promise!)
|
||||||
|
const cache = children.vcsCache.get(directory)
|
||||||
if (!cache) return
|
if (!cache) return
|
||||||
const sdk = sdkFor(directory)
|
const sdk = sdkFor(directory)
|
||||||
await bootstrapDirectory({
|
await bootstrapDirectory({
|
||||||
@@ -322,17 +286,16 @@ function createGlobalSync() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
booting.set(key, promise)
|
booting.set(directory, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
booting.delete(key)
|
booting.delete(directory)
|
||||||
children.unpin(key)
|
children.unpin(directory)
|
||||||
})
|
})
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsub = globalSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
const directory = e.name
|
const directory = e.name
|
||||||
const key = directoryKey(directory)
|
|
||||||
const event = e.details
|
const event = e.details
|
||||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||||
|
|
||||||
@@ -342,7 +305,7 @@ function createGlobalSync() {
|
|||||||
project: globalStore.project,
|
project: globalStore.project,
|
||||||
refresh: () => {
|
refresh: () => {
|
||||||
if (recent) return
|
if (recent) return
|
||||||
bootstrap.refetch()
|
queue.refresh()
|
||||||
},
|
},
|
||||||
setGlobalProject: setProjects,
|
setGlobalProject: setProjects,
|
||||||
})
|
})
|
||||||
@@ -355,9 +318,9 @@ function createGlobalSync() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = children.children[key]
|
const existing = children.children[directory]
|
||||||
if (!existing) return
|
if (!existing) return
|
||||||
children.mark(key)
|
children.mark(directory)
|
||||||
const [store, setStore] = existing
|
const [store, setStore] = existing
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event,
|
event,
|
||||||
@@ -366,9 +329,14 @@ function createGlobalSync() {
|
|||||||
setStore,
|
setStore,
|
||||||
push: queue.push,
|
push: queue.push,
|
||||||
setSessionTodo,
|
setSessionTodo,
|
||||||
vcsCache: children.vcsCache.get(key),
|
vcsCache: children.vcsCache.get(directory),
|
||||||
loadLsp: () => {
|
loadLsp: () => {
|
||||||
void queryClient.fetchQuery(loadLspQuery(key, sdkFor(directory)))
|
void sdkFor(directory)
|
||||||
|
.lsp.status()
|
||||||
|
.then((x) => {
|
||||||
|
setStore("lsp", x.data ?? [])
|
||||||
|
setStore("lsp_ready", true)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -379,10 +347,27 @@ function createGlobalSync() {
|
|||||||
})
|
})
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
for (const directory of Object.keys(children.children)) {
|
for (const directory of Object.keys(children.children)) {
|
||||||
children.disposeDirectory(directoryKey(directory))
|
children.disposeDirectory(directory)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
bootingRoot = true
|
||||||
|
try {
|
||||||
|
await bootstrapGlobal({
|
||||||
|
globalSDK: globalSDK.client,
|
||||||
|
requestFailedTitle: language.t("common.requestFailed"),
|
||||||
|
translate: language.t,
|
||||||
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||||
|
setGlobalStore: setBootStore,
|
||||||
|
queryClient,
|
||||||
|
})
|
||||||
|
bootedAt = Date.now()
|
||||||
|
} finally {
|
||||||
|
bootingRoot = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (typeof requestAnimationFrame === "function") {
|
if (typeof requestAnimationFrame === "function") {
|
||||||
eventFrame = requestAnimationFrame(() => {
|
eventFrame = requestAnimationFrame(() => {
|
||||||
@@ -398,6 +383,7 @@ function createGlobalSync() {
|
|||||||
void globalSDK.event.start()
|
void globalSDK.event.start()
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
void bootstrap()
|
||||||
})
|
})
|
||||||
|
|
||||||
const projectApi = {
|
const projectApi = {
|
||||||
@@ -410,10 +396,21 @@ function createGlobalSync() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateConfigMutation = useMutation(() => ({
|
const updateConfig = async (config: Config) => {
|
||||||
mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }),
|
setGlobalStore("reload", "pending")
|
||||||
onSuccess: () => bootstrap.refetch(),
|
return globalSDK.client.global.config
|
||||||
}))
|
.update({ config })
|
||||||
|
.then(bootstrap)
|
||||||
|
.then(() => {
|
||||||
|
queue.refresh()
|
||||||
|
setGlobalStore("reload", undefined)
|
||||||
|
queue.refresh()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setGlobalStore("reload", undefined)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: globalStore,
|
data: globalStore,
|
||||||
@@ -426,8 +423,8 @@ function createGlobalSync() {
|
|||||||
},
|
},
|
||||||
child: children.child,
|
child: children.child,
|
||||||
peek: children.peek,
|
peek: children.peek,
|
||||||
// bootstrap,
|
bootstrap,
|
||||||
updateConfig: updateConfigMutation.mutateAsync,
|
updateConfig,
|
||||||
project: projectApi,
|
project: projectApi,
|
||||||
todo: {
|
todo: {
|
||||||
set: setSessionTodo,
|
set: setSessionTodo,
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ import type {
|
|||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { retry } from "@opencode-ai/core/util/retry"
|
import { retry } from "@opencode-ai/shared/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 type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||||
import { loadMcpQuery } from "../global-sync"
|
import { loadSessionsQuery } from "../global-sync"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -67,43 +67,6 @@ function runAll(list: Array<() => Promise<unknown>>) {
|
|||||||
return Promise.allSettled(list.map((item) => item()))
|
return Promise.allSettled(list.map((item) => item()))
|
||||||
}
|
}
|
||||||
|
|
||||||
function showErrors(input: {
|
|
||||||
errors: unknown[]
|
|
||||||
title: string
|
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
|
||||||
formatMoreCount: (count: number) => string
|
|
||||||
}) {
|
|
||||||
if (input.errors.length === 0) return
|
|
||||||
const message = formatServerError(input.errors[0], input.translate)
|
|
||||||
const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: input.title,
|
|
||||||
description: message + more,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadGlobalConfigQuery = (sdk: OpencodeClient) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: ["config"],
|
|
||||||
queryFn: () => retry(() => sdk.global.config.get().then((x) => x.data!)),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadProjectsQuery = (sdk: OpencodeClient) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: ["project"],
|
|
||||||
queryFn: () =>
|
|
||||||
retry(() =>
|
|
||||||
sdk.project.list().then((x) => {
|
|
||||||
return (x.data ?? [])
|
|
||||||
.filter((p) => !!p?.id)
|
|
||||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => cmp(a.id, b.id))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function bootstrapGlobal(input: {
|
export async function bootstrapGlobal(input: {
|
||||||
globalSDK: OpencodeClient
|
globalSDK: OpencodeClient
|
||||||
requestFailedTitle: string
|
requestFailedTitle: string
|
||||||
@@ -112,15 +75,53 @@ export async function bootstrapGlobal(input: {
|
|||||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const slow = [
|
const fast = [
|
||||||
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)),
|
|
||||||
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)),
|
|
||||||
() => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)),
|
|
||||||
() =>
|
() =>
|
||||||
input.queryClient
|
retry(() =>
|
||||||
.fetchQuery(loadProjectsQuery(input.globalSDK))
|
input.globalSDK.global.config.get().then((x) => {
|
||||||
.then((data) => input.setGlobalStore("project", data)),
|
input.setGlobalStore("config", x.data!)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
input.queryClient.fetchQuery({
|
||||||
|
...loadProvidersQuery(null),
|
||||||
|
queryFn: () =>
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.provider.list().then((x) => {
|
||||||
|
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||||
|
return null
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const slow = [
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.path.get().then((x) => {
|
||||||
|
input.setGlobalStore("path", x.data!)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.project.list().then((x) => {
|
||||||
|
const projects = (x.data ?? [])
|
||||||
|
.filter((p) => !!p?.id)
|
||||||
|
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
|
input.setGlobalStore("project", projects)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
await runAll(fast)
|
||||||
|
// showErrors({
|
||||||
|
// errors: errors(await runAll(fast)),
|
||||||
|
// title: input.requestFailedTitle,
|
||||||
|
// translate: input.translate,
|
||||||
|
// formatMoreCount: input.formatMoreCount,
|
||||||
|
// })
|
||||||
|
await waitForPaint()
|
||||||
await runAll(slow)
|
await runAll(slow)
|
||||||
// showErrors({
|
// showErrors({
|
||||||
// errors: errors(),
|
// errors: errors(),
|
||||||
@@ -128,6 +129,7 @@ export async function bootstrapGlobal(input: {
|
|||||||
// translate: input.translate,
|
// translate: input.translate,
|
||||||
// formatMoreCount: input.formatMoreCount,
|
// formatMoreCount: input.formatMoreCount,
|
||||||
// })
|
// })
|
||||||
|
input.setGlobalStore("ready", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
|
function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
|
||||||
@@ -178,23 +180,11 @@ function warmSessions(input: {
|
|||||||
).then(() => undefined)
|
).then(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadProvidersQuery = (directory: string | null, sdk: OpencodeClient) =>
|
export const loadProvidersQuery = (directory: string | null) =>
|
||||||
queryOptions({
|
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||||
queryKey: [directory, "providers"],
|
|
||||||
queryFn: () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadAgentsQuery = (directory: string | null, sdk: OpencodeClient) =>
|
export const loadAgentsQuery = (directory: string | null) =>
|
||||||
queryOptions({
|
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||||
queryKey: [directory, "agents"],
|
|
||||||
queryFn: () => retry(() => sdk.app.agents().then((x) => normalizeAgentList(x.data))),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadPathQuery = (directory: string | null, sdk: OpencodeClient) =>
|
|
||||||
queryOptions<Path>({
|
|
||||||
queryKey: [directory, "path"],
|
|
||||||
queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)),
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
@@ -217,31 +207,60 @@ export async function bootstrapDirectory(input: {
|
|||||||
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
|
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
|
||||||
if (seededProject) input.setStore("project", seededProject)
|
if (seededProject) input.setStore("project", seededProject)
|
||||||
if (seededPath) input.setStore("path", seededPath)
|
if (seededPath) input.setStore("path", seededPath)
|
||||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) {
|
||||||
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
input.setStore("provider", input.global.provider)
|
||||||
}
|
}
|
||||||
|
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||||
|
input.setStore("config", input.global.config)
|
||||||
|
}
|
||||||
|
if (loading || input.store.provider.all.length === 0) {
|
||||||
|
input.setStore("provider_ready", false)
|
||||||
|
}
|
||||||
|
input.setStore("mcp_ready", false)
|
||||||
|
input.setStore("mcp", {})
|
||||||
|
input.setStore("lsp_ready", false)
|
||||||
|
input.setStore("lsp", [])
|
||||||
if (loading) input.setStore("status", "partial")
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||||
providerRev.set(input.directory, rev)
|
|
||||||
|
const errs = errors(await runAll(fast))
|
||||||
|
if (errs.length > 0) {
|
||||||
|
console.error("Failed to bootstrap instance", errs[0])
|
||||||
|
const project = getFilename(input.directory)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
|
description: formatServerError(errs[0], input.translate),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const slow = [
|
const slow = [
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
|
||||||
() =>
|
() =>
|
||||||
input.queryClient
|
input.queryClient.ensureQueryData({
|
||||||
.ensureQueryData(loadAgentsQuery(input.directory, input.sdk))
|
...loadAgentsQuery(input.directory),
|
||||||
.then((data) => input.setStore("agent", data)),
|
queryFn: () =>
|
||||||
() =>
|
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||||
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
|
() => null,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||||
!seededProject &&
|
() =>
|
||||||
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
seededProject
|
||||||
!seededPath &&
|
? Promise.resolve()
|
||||||
(() =>
|
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||||
input.queryClient.ensureQueryData(loadPathQuery(input.directory, input.sdk)).then((data) => {
|
() =>
|
||||||
const next = projectID(data.directory ?? input.directory, input.global.project)
|
seededPath
|
||||||
if (next) input.setStore("project", next)
|
? Promise.resolve()
|
||||||
})),
|
: retry(() =>
|
||||||
|
input.sdk.path.get().then((x) => {
|
||||||
|
input.setStore("path", x.data!)
|
||||||
|
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||||
|
if (next) input.setStore("project", next)
|
||||||
|
}),
|
||||||
|
),
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.sdk.vcs.get().then((x) => {
|
input.sdk.vcs.get().then((x) => {
|
||||||
@@ -304,17 +323,14 @@ export async function bootstrapDirectory(input: {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
() => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk)),
|
|
||||||
() =>
|
() =>
|
||||||
input.queryClient.fetchQuery(loadProvidersQuery(input.directory, input.sdk)).catch((err) => {
|
retry(() =>
|
||||||
const project = getFilename(input.directory)
|
input.sdk.mcp.status().then((x) => {
|
||||||
showToast({
|
input.setStore("mcp", x.data!)
|
||||||
variant: "error",
|
input.setStore("mcp_ready", true)
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
}),
|
||||||
description: formatServerError(err, input.translate),
|
),
|
||||||
})
|
]
|
||||||
}),
|
|
||||||
].filter(Boolean) as (() => Promise<any>)[]
|
|
||||||
|
|
||||||
await waitForPaint()
|
await waitForPaint()
|
||||||
const slowErrs = errors(await runAll(slow))
|
const slowErrs = errors(await runAll(slow))
|
||||||
@@ -328,6 +344,29 @@ export async function bootstrapDirectory(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||||
|
|
||||||
|
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||||
|
providerRev.set(input.directory, rev)
|
||||||
|
void input.queryClient.ensureQueryData({
|
||||||
|
...loadSessionsQuery(input.directory),
|
||||||
|
queryFn: () =>
|
||||||
|
retry(() => input.sdk.provider.list())
|
||||||
|
.then((x) => {
|
||||||
|
if (providerRev.get(input.directory) !== rev) return
|
||||||
|
input.setStore("provider", normalizeProviderList(x.data!))
|
||||||
|
input.setStore("provider_ready", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||||
|
const project = getFilename(input.directory)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
|
description: formatServerError(err, input.translate),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(() => null),
|
||||||
|
})
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ describe("createChildStoreManager", () => {
|
|||||||
onBootstrap() {},
|
onBootstrap() {},
|
||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
getSdk: () => null!,
|
|
||||||
global: { provider: null! },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import type { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||||
import {
|
import {
|
||||||
DIR_IDLE_TTL_MS,
|
DIR_IDLE_TTL_MS,
|
||||||
MAX_DIR_STORES,
|
MAX_DIR_STORES,
|
||||||
@@ -14,10 +14,6 @@ import {
|
|||||||
type VcsCache,
|
type VcsCache,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||||
import { useQueries } from "@tanstack/solid-query"
|
|
||||||
import { loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
|
||||||
import { loadLspQuery, loadMcpQuery } from "../global-sync"
|
|
||||||
import { directoryKey, type DirectoryKey } from "./utils"
|
|
||||||
|
|
||||||
export function createChildStoreManager(input: {
|
export function createChildStoreManager(input: {
|
||||||
owner: Owner
|
owner: Owner
|
||||||
@@ -26,10 +22,6 @@ export function createChildStoreManager(input: {
|
|||||||
onBootstrap: (directory: string) => void
|
onBootstrap: (directory: string) => void
|
||||||
onDispose: (directory: string) => void
|
onDispose: (directory: string) => void
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
getSdk: (directory: string) => OpencodeClient
|
|
||||||
global: {
|
|
||||||
provider: ProviderListResponse
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||||
const vcsCache = new Map<string, VcsCache>()
|
const vcsCache = new Map<string, VcsCache>()
|
||||||
@@ -40,37 +32,30 @@ export function createChildStoreManager(input: {
|
|||||||
const ownerPins = new WeakMap<object, Set<string>>()
|
const ownerPins = new WeakMap<object, Set<string>>()
|
||||||
const disposers = new Map<string, () => void>()
|
const disposers = new Map<string, () => void>()
|
||||||
|
|
||||||
const markKey = (key: DirectoryKey) => {
|
|
||||||
if (!key) return
|
|
||||||
lifecycle.set(key, { lastAccessAt: Date.now() })
|
|
||||||
runEviction(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mark = (directory: string) => {
|
const mark = (directory: string) => {
|
||||||
const key = directoryKey(directory)
|
if (!directory) return
|
||||||
markKey(key)
|
lifecycle.set(directory, { lastAccessAt: Date.now() })
|
||||||
|
runEviction(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pin = (directory: string) => {
|
const pin = (directory: string) => {
|
||||||
const key = directoryKey(directory)
|
if (!directory) return
|
||||||
if (!key) return
|
pins.set(directory, (pins.get(directory) ?? 0) + 1)
|
||||||
pins.set(key, (pins.get(key) ?? 0) + 1)
|
mark(directory)
|
||||||
markKey(key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpin = (directory: string) => {
|
const unpin = (directory: string) => {
|
||||||
const key = directoryKey(directory)
|
if (!directory) return
|
||||||
if (!key) return
|
const next = (pins.get(directory) ?? 0) - 1
|
||||||
const next = (pins.get(key) ?? 0) - 1
|
|
||||||
if (next > 0) {
|
if (next > 0) {
|
||||||
pins.set(key, next)
|
pins.set(directory, next)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pins.delete(key)
|
pins.delete(directory)
|
||||||
runEviction()
|
runEviction()
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinned = (directory: string) => (pins.get(directoryKey(directory)) ?? 0) > 0
|
const pinned = (directory: string) => (pins.get(directory) ?? 0) > 0
|
||||||
|
|
||||||
const pinForOwner = (directory: string) => {
|
const pinForOwner = (directory: string) => {
|
||||||
const current = getOwner()
|
const current = getOwner()
|
||||||
@@ -92,31 +77,30 @@ export function createChildStoreManager(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function disposeDirectory(directory: DirectoryKey) {
|
function disposeDirectory(directory: string) {
|
||||||
const key = directory
|
|
||||||
if (
|
if (
|
||||||
!canDisposeDirectory({
|
!canDisposeDirectory({
|
||||||
directory: key,
|
directory,
|
||||||
hasStore: !!children[key],
|
hasStore: !!children[directory],
|
||||||
pinned: pinned(key),
|
pinned: pinned(directory),
|
||||||
booting: input.isBooting(key),
|
booting: input.isBooting(directory),
|
||||||
loadingSessions: input.isLoadingSessions(key),
|
loadingSessions: input.isLoadingSessions(directory),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
vcsCache.delete(key)
|
vcsCache.delete(directory)
|
||||||
metaCache.delete(key)
|
metaCache.delete(directory)
|
||||||
iconCache.delete(key)
|
iconCache.delete(directory)
|
||||||
lifecycle.delete(key)
|
lifecycle.delete(directory)
|
||||||
const dispose = disposers.get(key)
|
const dispose = disposers.get(directory)
|
||||||
if (dispose) {
|
if (dispose) {
|
||||||
dispose()
|
dispose()
|
||||||
disposers.delete(key)
|
disposers.delete(directory)
|
||||||
}
|
}
|
||||||
delete children[key]
|
delete children[directory]
|
||||||
input.onDispose(key)
|
input.onDispose(directory)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,14 +117,13 @@ export function createChildStoreManager(input: {
|
|||||||
}).filter((directory) => directory !== skip)
|
}).filter((directory) => directory !== skip)
|
||||||
if (list.length === 0) return
|
if (list.length === 0) return
|
||||||
for (const directory of list) {
|
for (const directory of list) {
|
||||||
if (!disposeDirectory(directoryKey(directory))) continue
|
if (!disposeDirectory(directory)) continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureChild(directory: string) {
|
function ensureChild(directory: string) {
|
||||||
const key = directoryKey(directory)
|
if (!directory) console.error("No directory provided")
|
||||||
if (!key) console.error("No directory provided")
|
if (!children[directory]) {
|
||||||
if (!children[key]) {
|
|
||||||
const vcs = runWithOwner(input.owner, () =>
|
const vcs = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
||||||
@@ -149,7 +132,7 @@ export function createChildStoreManager(input: {
|
|||||||
)
|
)
|
||||||
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
|
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
|
||||||
const vcsStore = vcs[0]
|
const vcsStore = vcs[0]
|
||||||
vcsCache.set(key, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
||||||
|
|
||||||
const meta = runWithOwner(input.owner, () =>
|
const meta = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
@@ -158,7 +141,7 @@ export function createChildStoreManager(input: {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
|
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
|
||||||
metaCache.set(key, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
||||||
|
|
||||||
const icon = runWithOwner(input.owner, () =>
|
const icon = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
@@ -167,44 +150,20 @@ export function createChildStoreManager(input: {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
|
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
|
||||||
iconCache.set(key, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
||||||
|
|
||||||
const init = () =>
|
const init = () =>
|
||||||
createRoot((dispose) => {
|
createRoot((dispose) => {
|
||||||
const sdk = input.getSdk(directory)
|
|
||||||
|
|
||||||
const initialMeta = meta[0].value
|
const initialMeta = meta[0].value
|
||||||
const initialIcon = icon[0].value
|
const initialIcon = icon[0].value
|
||||||
|
|
||||||
const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
|
|
||||||
queries: [
|
|
||||||
loadPathQuery(key, sdk),
|
|
||||||
loadMcpQuery(key, sdk),
|
|
||||||
loadLspQuery(key, sdk),
|
|
||||||
loadProvidersQuery(key, sdk),
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
const child = createStore<State>({
|
const child = createStore<State>({
|
||||||
project: "",
|
project: "",
|
||||||
projectMeta: initialMeta,
|
projectMeta: initialMeta,
|
||||||
icon: initialIcon,
|
icon: initialIcon,
|
||||||
get provider_ready() {
|
provider_ready: false,
|
||||||
return !providerQuery.isLoading
|
provider: { all: [], connected: [], default: {} },
|
||||||
},
|
|
||||||
get provider() {
|
|
||||||
const EMPTY = { all: [], connected: [], default: {} }
|
|
||||||
if (providerQuery.isLoading) return EMPTY
|
|
||||||
if (providerQuery.data?.all.length === 0 && input.global.provider.all.length > 0)
|
|
||||||
return input.global.provider
|
|
||||||
return providerQuery.data ?? EMPTY
|
|
||||||
},
|
|
||||||
config: {},
|
config: {},
|
||||||
get path() {
|
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||||
if (pathQuery.isLoading || !pathQuery.data)
|
|
||||||
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
|
||||||
return pathQuery.data
|
|
||||||
},
|
|
||||||
status: "loading" as const,
|
status: "loading" as const,
|
||||||
agent: [],
|
agent: [],
|
||||||
command: [],
|
command: [],
|
||||||
@@ -215,30 +174,23 @@ export function createChildStoreManager(input: {
|
|||||||
todo: {},
|
todo: {},
|
||||||
permission: {},
|
permission: {},
|
||||||
question: {},
|
question: {},
|
||||||
get mcp_ready() {
|
mcp_ready: false,
|
||||||
return !mcpQuery.isLoading
|
mcp: {},
|
||||||
},
|
lsp_ready: false,
|
||||||
get mcp() {
|
lsp: [],
|
||||||
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
|
|
||||||
},
|
|
||||||
get lsp_ready() {
|
|
||||||
return !lspQuery.isLoading
|
|
||||||
},
|
|
||||||
get lsp() {
|
|
||||||
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
|
|
||||||
},
|
|
||||||
vcs: vcsStore.value,
|
vcs: vcsStore.value,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
|
bootstrapPromise: Promise.resolve(),
|
||||||
})
|
})
|
||||||
children[key] = child
|
children[directory] = child
|
||||||
disposers.set(key, dispose)
|
disposers.set(directory, dispose)
|
||||||
|
|
||||||
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
||||||
if (!(init instanceof Promise)) return
|
if (!(init instanceof Promise)) return
|
||||||
void init.then(() => {
|
void init.then(() => {
|
||||||
if (children[key] !== child) return
|
if (children[directory] !== child) return
|
||||||
run()
|
run()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -262,16 +214,15 @@ export function createChildStoreManager(input: {
|
|||||||
|
|
||||||
runWithOwner(input.owner, init)
|
runWithOwner(input.owner, init)
|
||||||
}
|
}
|
||||||
markKey(key)
|
mark(directory)
|
||||||
const childStore = children[key]
|
const childStore = children[directory]
|
||||||
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
|
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
|
||||||
return childStore
|
return childStore
|
||||||
}
|
}
|
||||||
|
|
||||||
function child(directory: string, options: ChildOptions = {}) {
|
function child(directory: string, options: ChildOptions = {}) {
|
||||||
const key = directoryKey(directory)
|
|
||||||
const childStore = ensureChild(directory)
|
const childStore = ensureChild(directory)
|
||||||
pinForOwner(key)
|
pinForOwner(directory)
|
||||||
const shouldBootstrap = options.bootstrap ?? true
|
const shouldBootstrap = options.bootstrap ?? true
|
||||||
if (shouldBootstrap && childStore[0].status === "loading") {
|
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||||
input.onBootstrap(directory)
|
input.onBootstrap(directory)
|
||||||
@@ -280,7 +231,6 @@ export function createChildStoreManager(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function peek(directory: string, options: ChildOptions = {}) {
|
function peek(directory: string, options: ChildOptions = {}) {
|
||||||
const key = directoryKey(directory)
|
|
||||||
const childStore = ensureChild(directory)
|
const childStore = ensureChild(directory)
|
||||||
const shouldBootstrap = options.bootstrap ?? true
|
const shouldBootstrap = options.bootstrap ?? true
|
||||||
if (shouldBootstrap && childStore[0].status === "loading") {
|
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||||
@@ -290,9 +240,8 @@ export function createChildStoreManager(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function projectMeta(directory: string, patch: ProjectMeta) {
|
function projectMeta(directory: string, patch: ProjectMeta) {
|
||||||
const key = directoryKey(directory)
|
|
||||||
const [store, setStore] = ensureChild(directory)
|
const [store, setStore] = ensureChild(directory)
|
||||||
const cached = metaCache.get(key)
|
const cached = metaCache.get(directory)
|
||||||
if (!cached) return
|
if (!cached) return
|
||||||
const previous = store.projectMeta ?? {}
|
const previous = store.projectMeta ?? {}
|
||||||
const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon
|
const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon
|
||||||
@@ -308,9 +257,8 @@ export function createChildStoreManager(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function projectIcon(directory: string, value: string | undefined) {
|
function projectIcon(directory: string, value: string | undefined) {
|
||||||
const key = directoryKey(directory)
|
|
||||||
const [store, setStore] = ensureChild(directory)
|
const [store, setStore] = ensureChild(directory)
|
||||||
const cached = iconCache.get(key)
|
const cached = iconCache.get(directory)
|
||||||
if (!cached) return
|
if (!cached) return
|
||||||
if (store.icon === value) return
|
if (store.icon === value) return
|
||||||
cached.setStore("value", value)
|
cached.setStore("value", value)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/shared/util/binary"
|
||||||
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
@@ -21,7 +21,7 @@ const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
|||||||
export function applyGlobalEvent(input: {
|
export function applyGlobalEvent(input: {
|
||||||
event: { type: string; properties?: unknown }
|
event: { type: string; properties?: unknown }
|
||||||
project: Project[]
|
project: Project[]
|
||||||
setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void
|
setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => void
|
||||||
refresh: () => void
|
refresh: () => void
|
||||||
}) {
|
}) {
|
||||||
if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
|
if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
|
||||||
@@ -33,18 +33,14 @@ export function applyGlobalEvent(input: {
|
|||||||
const properties = input.event.properties as Project
|
const properties = input.event.properties as Project
|
||||||
const result = Binary.search(input.project, properties.id, (s) => s.id)
|
const result = Binary.search(input.project, properties.id, (s) => s.id)
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
input.setGlobalProject(
|
input.setGlobalProject((draft) => {
|
||||||
produce((draft) => {
|
draft[result.index] = { ...draft[result.index], ...properties }
|
||||||
draft[result.index] = { ...draft[result.index], ...properties }
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
input.setGlobalProject(
|
input.setGlobalProject((draft) => {
|
||||||
produce((draft) => {
|
draft.splice(result.index, 0, properties)
|
||||||
draft.splice(result.index, 0, properties)
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupSessionCaches(
|
function cleanupSessionCaches(
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { createRefreshQueue } from "./queue"
|
|
||||||
import { directoryKey } from "./utils"
|
|
||||||
|
|
||||||
const tick = () => new Promise((resolve) => setTimeout(resolve, 10))
|
|
||||||
|
|
||||||
describe("createRefreshQueue", () => {
|
|
||||||
test("clears queued directories by normalized key", async () => {
|
|
||||||
const calls: string[] = []
|
|
||||||
const queue = createRefreshQueue({
|
|
||||||
paused: () => false,
|
|
||||||
key: directoryKey,
|
|
||||||
bootstrap: async () => {},
|
|
||||||
bootstrapInstance: (directory) => {
|
|
||||||
calls.push(directory)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
queue.push("C:\\tmp\\demo")
|
|
||||||
queue.clear("C:/tmp/demo")
|
|
||||||
|
|
||||||
await tick()
|
|
||||||
|
|
||||||
expect(calls).toEqual([])
|
|
||||||
queue.dispose()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("passes the original directory to bootstrapInstance", async () => {
|
|
||||||
const calls: string[] = []
|
|
||||||
const queue = createRefreshQueue({
|
|
||||||
paused: () => false,
|
|
||||||
key: directoryKey,
|
|
||||||
bootstrap: async () => {},
|
|
||||||
bootstrapInstance: (directory) => {
|
|
||||||
calls.push(directory)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
queue.push("C:\\tmp\\demo")
|
|
||||||
|
|
||||||
await tick()
|
|
||||||
|
|
||||||
expect(calls).toEqual(["C:\\tmp\\demo"])
|
|
||||||
queue.dispose()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -2,25 +2,22 @@ type QueueInput = {
|
|||||||
paused: () => boolean
|
paused: () => boolean
|
||||||
bootstrap: () => Promise<void>
|
bootstrap: () => Promise<void>
|
||||||
bootstrapInstance: (directory: string) => Promise<void> | void
|
bootstrapInstance: (directory: string) => Promise<void> | void
|
||||||
key?: (directory: string) => string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRefreshQueue(input: QueueInput) {
|
export function createRefreshQueue(input: QueueInput) {
|
||||||
const queued = new Map<string, string>()
|
const queued = new Set<string>()
|
||||||
let root = false
|
let root = false
|
||||||
let running = false
|
let running = false
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
const key = input.key ?? ((directory: string) => directory)
|
|
||||||
|
|
||||||
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
|
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||||
|
|
||||||
const take = (count: number) => {
|
const take = (count: number) => {
|
||||||
if (queued.size === 0) return [] as string[]
|
if (queued.size === 0) return [] as string[]
|
||||||
const items: string[] = []
|
const items: string[] = []
|
||||||
for (const [id, directory] of queued) {
|
for (const item of queued) {
|
||||||
queued.delete(id)
|
queued.delete(item)
|
||||||
items.push(directory)
|
items.push(item)
|
||||||
if (items.length >= count) break
|
if (items.length >= count) break
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
@@ -36,7 +33,7 @@ export function createRefreshQueue(input: QueueInput) {
|
|||||||
|
|
||||||
const push = (directory: string) => {
|
const push = (directory: string) => {
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
queued.set(key(directory), directory)
|
queued.add(directory)
|
||||||
if (input.paused()) return
|
if (input.paused()) return
|
||||||
schedule()
|
schedule()
|
||||||
}
|
}
|
||||||
@@ -76,7 +73,7 @@ export function createRefreshQueue(input: QueueInput) {
|
|||||||
push,
|
push,
|
||||||
refresh,
|
refresh,
|
||||||
clear(directory: string) {
|
clear(directory: string) {
|
||||||
queued.delete(key(directory))
|
queued.delete(directory)
|
||||||
},
|
},
|
||||||
dispose() {
|
dispose() {
|
||||||
if (!timer) return
|
if (!timer) return
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export type State = {
|
|||||||
part: {
|
part: {
|
||||||
[messageID: string]: Part[]
|
[messageID: string]: Part[]
|
||||||
}
|
}
|
||||||
|
bootstrapPromise: Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VcsCache = {
|
export type VcsCache = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { Agent } from "@opencode-ai/sdk/v2/client"
|
import type { Agent } from "@opencode-ai/sdk/v2/client"
|
||||||
import { directoryKey, normalizeAgentList } from "./utils"
|
import { normalizeAgentList } from "./utils"
|
||||||
|
|
||||||
const agent = (name = "build") =>
|
const agent = (name = "build") =>
|
||||||
({
|
({
|
||||||
@@ -33,20 +33,3 @@ describe("normalizeAgentList", () => {
|
|||||||
expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")])
|
expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("directoryKey", () => {
|
|
||||||
test("normalizes slashes", () => {
|
|
||||||
expect(String(directoryKey("C:\\Repos\\sst\\opencode"))).toBe("C:/Repos/sst/opencode")
|
|
||||||
expect(String(directoryKey("C:/Repos/sst/opencode"))).toBe("C:/Repos/sst/opencode")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("preserves backslashes in posix paths", () => {
|
|
||||||
expect(String(directoryKey("/tmp/foo\\bar"))).toBe("/tmp/foo\\bar")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("trims trailing slashes without breaking roots", () => {
|
|
||||||
expect(String(directoryKey("C:/Repos/sst/opencode/"))).toBe("C:/Repos/sst/opencode")
|
|
||||||
expect(String(directoryKey("C:/"))).toBe("C:/")
|
|
||||||
expect(String(directoryKey("/"))).toBe("/")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
||||||
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
|
|
||||||
|
|
||||||
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
|
||||||
|
|||||||
@@ -391,14 +391,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
? globalSync.data.project.find((x) => x.id === projectID)
|
? globalSync.data.project.find((x) => x.id === projectID)
|
||||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||||
|
|
||||||
// Preserve local icon override from per-workspace localStorage cache (childStore.icon).
|
const local = childStore.projectMeta
|
||||||
// Without this, different subdirectories of the same git repo would share the same
|
const localOverride =
|
||||||
// icon from the database instead of using their individual overrides.
|
local?.name !== undefined ||
|
||||||
const base = { ...metadata, ...project }
|
local?.commands?.start !== undefined ||
|
||||||
if (childStore.icon) {
|
local?.icon?.override !== undefined ||
|
||||||
return { ...base, icon: { ...base.icon, override: childStore.icon } }
|
local?.icon?.color !== undefined
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
...metadata,
|
||||||
|
...project,
|
||||||
|
icon: {
|
||||||
|
url: metadata?.icon?.url,
|
||||||
|
override: metadata?.icon?.override ?? childStore.icon,
|
||||||
|
color: metadata?.icon?.color,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGlobal = projectID === "global" || (metadata?.id === undefined && localOverride)
|
||||||
|
if (!isGlobal) return base
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
id: base.id ?? "global",
|
||||||
|
name: local?.name,
|
||||||
|
commands: local?.commands,
|
||||||
|
icon: {
|
||||||
|
url: base.icon?.url,
|
||||||
|
override: local?.icon?.override,
|
||||||
|
color: local?.icon?.color,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return base
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const roots = createMemo(() => {
|
const roots = createMemo(() => {
|
||||||
@@ -493,7 +516,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
if (project.icon?.color || project.icon?.override || project.icon?.url) continue
|
if (project.icon?.color) continue
|
||||||
const worktree = project.worktree
|
const worktree = project.worktree
|
||||||
const existing = colors[worktree]
|
const existing = colors[worktree]
|
||||||
const color = existing ?? pickAvailableColor(used)
|
const color = existing ?? pickAvailableColor(used)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { batch, createEffect, createMemo } from "solid-js"
|
import { batch, createEffect, createMemo } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
@@ -382,7 +382,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
setSaved("session", session, {
|
setSaved("session", session, {
|
||||||
agent: msg.agent,
|
agent: msg.agent,
|
||||||
model: msg.model,
|
model: msg.model,
|
||||||
variant: msg.model?.variant ?? null,
|
variant: msg.model.variant ?? null,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { useGlobalSync } from "./global-sync"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/shared/util/binary"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
|
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
|
||||||
|
|
||||||
const session = (input: { id: string; parentID?: string }) =>
|
const session = (input: { id: string; parentID?: string }) =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
|
|
||||||
export function acceptKey(sessionID: string, directory?: string) {
|
export function acceptKey(sessionID: string, directory?: string) {
|
||||||
if (!directory) return sessionID
|
if (!directory) return sessionID
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ export type Platform = {
|
|||||||
/** Storage mechanism, defaults to localStorage */
|
/** Storage mechanism, defaults to localStorage */
|
||||||
storage?: (name?: string) => SyncStorage | AsyncStorage
|
storage?: (name?: string) => SyncStorage | AsyncStorage
|
||||||
|
|
||||||
/** Check for a downloadable desktop update */
|
/** Check for updates (Tauri only) */
|
||||||
checkUpdate?(): Promise<UpdateInfo>
|
checkUpdate?(): Promise<UpdateInfo>
|
||||||
|
|
||||||
/** Install the downloaded update using the platform restart flow */
|
/** Install updates (Tauri only) */
|
||||||
updateAndRestart?(): Promise<void>
|
update?(): Promise<void>
|
||||||
|
|
||||||
/** Fetch override */
|
/** Fetch override */
|
||||||
fetch?: typeof fetch
|
fetch?: typeof fetch
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/shared/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction } from "solid-js/store"
|
import { createStore, type SetStoreFunction } from "solid-js/store"
|
||||||
@@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ready,
|
ready,
|
||||||
current: () => store.prompt,
|
current: createMemo(() => store.prompt),
|
||||||
cursor: createMemo(() => store.cursor),
|
cursor: createMemo(() => store.cursor),
|
||||||
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
|
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
||||||
context: {
|
context: {
|
||||||
items: createMemo(() => store.context.items),
|
items: createMemo(() => store.context.items),
|
||||||
add(item: ContextItem) {
|
add(item: ContextItem) {
|
||||||
@@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
|
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: () => session().ready,
|
ready: () => session().ready(),
|
||||||
current: () => session().current(),
|
current: () => session().current(),
|
||||||
cursor: () => session().cursor(),
|
cursor: () => session().cursor(),
|
||||||
dirty: () => session().dirty(),
|
dirty: () => session().dirty(),
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { resolveServerList, ServerConnection } from "./server"
|
|
||||||
|
|
||||||
describe("resolveServerList", () => {
|
|
||||||
test("lets startup auth_token credentials override a persisted same-url server", () => {
|
|
||||||
const list = resolveServerList({
|
|
||||||
stored: [{ url: "https://server.example.test" }],
|
|
||||||
props: [
|
|
||||||
{
|
|
||||||
type: "http",
|
|
||||||
authToken: true,
|
|
||||||
http: {
|
|
||||||
url: "https://server.example.test",
|
|
||||||
username: "opencode",
|
|
||||||
password: "secret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(list).toHaveLength(1)
|
|
||||||
expect(list[0]?.type).toBe("http")
|
|
||||||
expect(list[0]?.http).toEqual({
|
|
||||||
url: "https://server.example.test",
|
|
||||||
username: "opencode",
|
|
||||||
password: "secret",
|
|
||||||
})
|
|
||||||
expect(list[0]?.type === "http" ? list[0].authToken : false).toBe(true)
|
|
||||||
expect(ServerConnection.key(list[0]!) as string).toBe("https://server.example.test")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("keeps persisted credentials when startup has no auth_token", () => {
|
|
||||||
const list = resolveServerList({
|
|
||||||
stored: [
|
|
||||||
{
|
|
||||||
url: "https://server.example.test",
|
|
||||||
username: "opencode",
|
|
||||||
password: "saved",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
props: [{ type: "http", http: { url: "https://server.example.test" } }],
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(list).toHaveLength(1)
|
|
||||||
expect(list[0]?.type).toBe("http")
|
|
||||||
expect(list[0]?.http).toEqual({
|
|
||||||
url: "https://server.example.test",
|
|
||||||
username: "opencode",
|
|
||||||
password: "saved",
|
|
||||||
})
|
|
||||||
expect(list[0]?.type === "http" ? list[0].authToken : true).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -33,33 +33,6 @@ function isLocalHost(url: string) {
|
|||||||
if (host === "localhost" || host === "127.0.0.1") return "local"
|
if (host === "localhost" || host === "127.0.0.1") return "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveServerList(input: {
|
|
||||||
props?: Array<ServerConnection.Any>
|
|
||||||
stored: StoredServer[]
|
|
||||||
}): Array<ServerConnection.Any> {
|
|
||||||
const servers = [
|
|
||||||
...input.stored.map((value) =>
|
|
||||||
typeof value === "string"
|
|
||||||
? {
|
|
||||||
type: "http" as const,
|
|
||||||
http: { url: value },
|
|
||||||
}
|
|
||||||
: value,
|
|
||||||
),
|
|
||||||
...(input.props ?? []),
|
|
||||||
]
|
|
||||||
|
|
||||||
const deduped = new Map<ServerConnection.Key, ServerConnection.Any>()
|
|
||||||
for (const value of servers) {
|
|
||||||
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
|
|
||||||
const key = ServerConnection.key(conn)
|
|
||||||
if (deduped.has(key) && conn.type === "http" && !conn.authToken) continue
|
|
||||||
deduped.set(key, conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...deduped.values()]
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace ServerConnection {
|
export namespace ServerConnection {
|
||||||
type Base = { displayName?: string }
|
type Base = { displayName?: string }
|
||||||
|
|
||||||
@@ -73,7 +46,6 @@ export namespace ServerConnection {
|
|||||||
export type Http = {
|
export type Http = {
|
||||||
type: "http"
|
type: "http"
|
||||||
http: HttpBase
|
http: HttpBase
|
||||||
authToken?: boolean
|
|
||||||
} & Base
|
} & Base
|
||||||
|
|
||||||
export type Sidecar = {
|
export type Sidecar = {
|
||||||
@@ -141,7 +113,26 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
|
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
|
||||||
|
|
||||||
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
||||||
return resolveServerList({ stored: store.list, props: props.servers })
|
const servers = [
|
||||||
|
...(props.servers ?? []),
|
||||||
|
...store.list.map((value) =>
|
||||||
|
typeof value === "string"
|
||||||
|
? {
|
||||||
|
type: "http" as const,
|
||||||
|
http: { url: value },
|
||||||
|
}
|
||||||
|
: value,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const deduped = new Map(
|
||||||
|
servers.map((value) => {
|
||||||
|
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
|
||||||
|
return [ServerConnection.key(conn), conn]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return [...deduped.values()]
|
||||||
})
|
})
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
@@ -183,7 +174,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
function add(input: ServerConnection.Http) {
|
function add(input: ServerConnection.Http) {
|
||||||
const url_ = normalizeServerUrl(input.http.url)
|
const url_ = normalizeServerUrl(input.http.url)
|
||||||
if (!url_) return
|
if (!url_) return
|
||||||
const conn: ServerConnection.Http = { ...input, authToken: undefined, http: { ...input.http, url: url_ } }
|
const conn = { ...input, http: { ...input.http, url: url_ } }
|
||||||
return batch(() => {
|
return batch(() => {
|
||||||
const existing = store.list.findIndex((x) => url(x) === url_)
|
const existing = store.list.findIndex((x) => url(x) === url_)
|
||||||
if (existing !== -1) {
|
if (existing !== -1) {
|
||||||
|
|||||||
@@ -23,15 +23,9 @@ export interface Settings {
|
|||||||
autoSave: boolean
|
autoSave: boolean
|
||||||
releaseNotes: boolean
|
releaseNotes: boolean
|
||||||
followup: "queue" | "steer"
|
followup: "queue" | "steer"
|
||||||
showFileTree: boolean
|
|
||||||
showNavigation: boolean
|
|
||||||
showSearch: boolean
|
|
||||||
showStatus: boolean
|
|
||||||
showTerminal: boolean
|
|
||||||
showReasoningSummaries: boolean
|
showReasoningSummaries: boolean
|
||||||
shellToolPartsExpanded: boolean
|
shellToolPartsExpanded: boolean
|
||||||
editToolPartsExpanded: boolean
|
editToolPartsExpanded: boolean
|
||||||
showSessionProgressBar: boolean
|
|
||||||
}
|
}
|
||||||
updates: {
|
updates: {
|
||||||
startup: boolean
|
startup: boolean
|
||||||
@@ -40,7 +34,6 @@ export interface Settings {
|
|||||||
fontSize: number
|
fontSize: number
|
||||||
mono: string
|
mono: string
|
||||||
sans: string
|
sans: string
|
||||||
terminal: string
|
|
||||||
}
|
}
|
||||||
keybinds: Record<string, string>
|
keybinds: Record<string, string>
|
||||||
permissions: {
|
permissions: {
|
||||||
@@ -52,17 +45,13 @@ export interface Settings {
|
|||||||
|
|
||||||
export const monoDefault = "System Mono"
|
export const monoDefault = "System Mono"
|
||||||
export const sansDefault = "System Sans"
|
export const sansDefault = "System Sans"
|
||||||
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
|
|
||||||
|
|
||||||
const monoFallback =
|
const monoFallback =
|
||||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||||
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
||||||
const terminalFallback =
|
|
||||||
'"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
|
||||||
|
|
||||||
const monoBase = monoFallback
|
const monoBase = monoFallback
|
||||||
const sansBase = sansFallback
|
const sansBase = sansFallback
|
||||||
const terminalBase = terminalFallback
|
|
||||||
|
|
||||||
function input(font: string | undefined) {
|
function input(font: string | undefined) {
|
||||||
return font ?? ""
|
return font ?? ""
|
||||||
@@ -95,28 +84,14 @@ export function sansFontFamily(font: string | undefined) {
|
|||||||
return stack(font, sansBase)
|
return stack(font, sansBase)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function terminalInput(font: string | undefined) {
|
|
||||||
return input(font)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function terminalFontFamily(font: string | undefined) {
|
|
||||||
return stack(font, terminalBase)
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultSettings: Settings = {
|
const defaultSettings: Settings = {
|
||||||
general: {
|
general: {
|
||||||
autoSave: true,
|
autoSave: true,
|
||||||
releaseNotes: true,
|
releaseNotes: true,
|
||||||
followup: "steer",
|
followup: "steer",
|
||||||
showFileTree: false,
|
|
||||||
showNavigation: false,
|
|
||||||
showSearch: false,
|
|
||||||
showStatus: false,
|
|
||||||
showTerminal: false,
|
|
||||||
showReasoningSummaries: false,
|
showReasoningSummaries: false,
|
||||||
shellToolPartsExpanded: false,
|
shellToolPartsExpanded: false,
|
||||||
editToolPartsExpanded: false,
|
editToolPartsExpanded: false,
|
||||||
showSessionProgressBar: true,
|
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
startup: true,
|
startup: true,
|
||||||
@@ -125,7 +100,6 @@ const defaultSettings: Settings = {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
mono: "",
|
mono: "",
|
||||||
sans: "",
|
sans: "",
|
||||||
terminal: "",
|
|
||||||
},
|
},
|
||||||
keybinds: {},
|
keybinds: {},
|
||||||
permissions: {
|
permissions: {
|
||||||
@@ -188,26 +162,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setFollowup(value: "queue" | "steer") {
|
setFollowup(value: "queue" | "steer") {
|
||||||
setStore("general", "followup", value === "queue" ? "steer" : value)
|
setStore("general", "followup", value === "queue" ? "steer" : value)
|
||||||
},
|
},
|
||||||
showFileTree: withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree),
|
|
||||||
setShowFileTree(value: boolean) {
|
|
||||||
setStore("general", "showFileTree", value)
|
|
||||||
},
|
|
||||||
showNavigation: withFallback(() => store.general?.showNavigation, defaultSettings.general.showNavigation),
|
|
||||||
setShowNavigation(value: boolean) {
|
|
||||||
setStore("general", "showNavigation", value)
|
|
||||||
},
|
|
||||||
showSearch: withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch),
|
|
||||||
setShowSearch(value: boolean) {
|
|
||||||
setStore("general", "showSearch", value)
|
|
||||||
},
|
|
||||||
showStatus: withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus),
|
|
||||||
setShowStatus(value: boolean) {
|
|
||||||
setStore("general", "showStatus", value)
|
|
||||||
},
|
|
||||||
showTerminal: withFallback(() => store.general?.showTerminal, defaultSettings.general.showTerminal),
|
|
||||||
setShowTerminal(value: boolean) {
|
|
||||||
setStore("general", "showTerminal", value)
|
|
||||||
},
|
|
||||||
showReasoningSummaries: withFallback(
|
showReasoningSummaries: withFallback(
|
||||||
() => store.general?.showReasoningSummaries,
|
() => store.general?.showReasoningSummaries,
|
||||||
defaultSettings.general.showReasoningSummaries,
|
defaultSettings.general.showReasoningSummaries,
|
||||||
@@ -229,13 +183,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setEditToolPartsExpanded(value: boolean) {
|
setEditToolPartsExpanded(value: boolean) {
|
||||||
setStore("general", "editToolPartsExpanded", value)
|
setStore("general", "editToolPartsExpanded", value)
|
||||||
},
|
},
|
||||||
showSessionProgressBar: withFallback(
|
|
||||||
() => store.general?.showSessionProgressBar,
|
|
||||||
defaultSettings.general.showSessionProgressBar,
|
|
||||||
),
|
|
||||||
setShowSessionProgressBar(value: boolean) {
|
|
||||||
setStore("general", "showSessionProgressBar", value)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
||||||
@@ -256,10 +203,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setUIFont(value: string) {
|
setUIFont(value: string) {
|
||||||
setStore("appearance", "sans", value.trim() ? value : "")
|
setStore("appearance", "sans", value.trim() ? value : "")
|
||||||
},
|
},
|
||||||
terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal),
|
|
||||||
setTerminalFont(value: string) {
|
|
||||||
setStore("appearance", "terminal", value.trim() ? value : "")
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
keybinds: {
|
keybinds: {
|
||||||
get: (action: string) => store.keybinds?.[action],
|
get: (action: string) => store.keybinds?.[action],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { batch, createMemo } from "solid-js"
|
import { batch, createMemo } from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/shared/util/binary"
|
||||||
import { retry } from "@opencode-ai/core/util/retry"
|
import { retry } from "@opencode-ai/shared/util/retry"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import {
|
import {
|
||||||
clearSessionPrefetch,
|
clearSessionPrefetch,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user