mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-27 11:14:29 +00:00
Compare commits
173 Commits
migrate-co
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a69380da55 | ||
|
|
dafc577539 | ||
|
|
f0d8374b77 | ||
|
|
97f024f3cc | ||
|
|
3705282f5e | ||
|
|
0133b2e603 | ||
|
|
0586ea28b6 | ||
|
|
24c710e1d9 | ||
|
|
608938f643 | ||
|
|
7c26914368 | ||
|
|
6b1e26db90 | ||
|
|
cb7b2bbbc6 | ||
|
|
58e2797e2e | ||
|
|
37c3360f62 | ||
|
|
6b3118883c | ||
|
|
6ec9ad6654 | ||
|
|
d6e4706678 | ||
|
|
ab44597018 | ||
|
|
0da8af8a28 | ||
|
|
2a4ed49551 | ||
|
|
7528419172 | ||
|
|
1d0d427b5f | ||
|
|
8c739b4a7d | ||
|
|
aec95c4d10 | ||
|
|
b2c82cb897 | ||
|
|
f2100dcfd8 | ||
|
|
b0b88f6792 | ||
|
|
e9a7c71141 | ||
|
|
4205fbd2aa | ||
|
|
fc52e4b2d3 | ||
|
|
9a6bfeb782 | ||
|
|
fa119423ec | ||
|
|
bf442a50c0 | ||
|
|
09e1b98bc6 | ||
|
|
37d42595cf | ||
|
|
adabad19f1 | ||
|
|
7a74be3b47 | ||
|
|
c95febb1d5 | ||
|
|
ecf17ac7c2 | ||
|
|
9736fce8fc | ||
|
|
d5ac2ea8be | ||
|
|
05d77b7d47 | ||
|
|
8c484a05b8 | ||
|
|
a0b3bbffd5 | ||
|
|
270d084cb1 | ||
|
|
9312867565 | ||
|
|
7e6a007c35 | ||
|
|
12dfd7e6a8 | ||
|
|
9398303767 | ||
|
|
7a8a32fc3c | ||
|
|
8b4eb642e2 | ||
|
|
9b9268d3b9 | ||
|
|
17259c0c24 | ||
|
|
14aaa7a626 | ||
|
|
9c578006c7 | ||
|
|
7b4180d153 | ||
|
|
3a3a4cdfe6 | ||
|
|
5745ee87ba | ||
|
|
08f056d412 | ||
|
|
96ca0de3bc | ||
|
|
a48b3d17dd | ||
|
|
04a6e432f5 | ||
|
|
ab640f10a0 | ||
|
|
b4d0090e00 | ||
|
|
05ac0a73e1 | ||
|
|
7453e78b35 | ||
|
|
bb8a1718a6 | ||
|
|
6b021658ad | ||
|
|
799b2623cb | ||
|
|
fce811b52f | ||
|
|
aae75b3cfb | ||
|
|
392a6d993f | ||
|
|
c4ea11fef3 | ||
|
|
b8337cddc4 | ||
|
|
444178e079 | ||
|
|
beef0115f1 | ||
|
|
4551282a4b | ||
|
|
9d29d692c6 | ||
|
|
1172fa418e | ||
|
|
b368181ac9 | ||
|
|
7afa48b4ef | ||
|
|
45191ad144 | ||
|
|
2869922696 | ||
|
|
e48c1ccf07 | ||
|
|
5e5823ed85 | ||
|
|
de2bc25677 | ||
|
|
79b5ce58e9 | ||
|
|
088a81c116 | ||
|
|
d848c9b6a3 | ||
|
|
561f9f5f05 | ||
|
|
3c6c74457d | ||
|
|
fc6e7934bd | ||
|
|
d7500b25b8 | ||
|
|
5d5f2cfee6 | ||
|
|
1172ebe697 | ||
|
|
d00d98d56a | ||
|
|
6fc5506293 | ||
|
|
bf53e1c24b | ||
|
|
50004d1f94 | ||
|
|
acd7c5ad55 | ||
|
|
cf54b544e3 | ||
|
|
76b60f3779 | ||
|
|
6af7ddf03b | ||
|
|
0b3fb5d460 | ||
|
|
a487f11a30 | ||
|
|
637059a515 | ||
|
|
fa559b0385 | ||
|
|
814c1d398c | ||
|
|
da40ab7b3d | ||
|
|
e718263778 | ||
|
|
3af12c53c4 | ||
|
|
825c9c3914 | ||
|
|
aebf885f39 | ||
|
|
29ddd55088 | ||
|
|
2c00eb60bd | ||
|
|
2a87860c06 | ||
|
|
68cf011fd3 | ||
|
|
f8cfb697bd | ||
|
|
c6d8e7624d | ||
|
|
0d0d0578eb | ||
|
|
cc02476ea5 | ||
|
|
5190589632 | ||
|
|
c92913e962 | ||
|
|
082f0cc127 | ||
|
|
2cee947671 | ||
|
|
e27d3d5d40 | ||
|
|
32417774c4 | ||
|
|
36197f5ff8 | ||
|
|
3d379c20c4 | ||
|
|
06f25c78f6 | ||
|
|
1a0639e5b8 | ||
|
|
1af3e9e557 | ||
|
|
a292eddeb5 | ||
|
|
79254c1020 | ||
|
|
ef7f222d80 | ||
|
|
888b123387 | ||
|
|
13cabae29f | ||
|
|
659068942e | ||
|
|
3201a7d34b | ||
|
|
de796d9a00 | ||
|
|
a592bd9684 | ||
|
|
744059a00f | ||
|
|
fb6d201ee0 | ||
|
|
cda2af2589 | ||
|
|
eda71373b0 | ||
|
|
cf5cfb48cd | ||
|
|
ae190038f8 | ||
|
|
0269f39a17 | ||
|
|
0a91196919 | ||
|
|
284251ad66 | ||
|
|
34495a70d5 | ||
|
|
ad5f0816a3 | ||
|
|
24c63914bf | ||
|
|
52b42258fa | ||
|
|
3026a005b6 | ||
|
|
a6f802d7fe | ||
|
|
9ef803be82 | ||
|
|
ce5c827a6e | ||
|
|
56decd79db | ||
|
|
9d78b69cd3 | ||
|
|
e31f00ad22 | ||
|
|
a90e8de050 | ||
|
|
eabf770053 | ||
|
|
86d7bdc542 | ||
|
|
d3ab78bba0 | ||
|
|
a531f3f36d | ||
|
|
bb3382311d | ||
|
|
ad545d0cc9 | ||
|
|
ac244b1458 | ||
|
|
f202536b65 | ||
|
|
405cc3f610 | ||
|
|
878c1b8c2d | ||
|
|
bb4d978684 |
17
.github/actions/setup-bun/action.yml
vendored
17
.github/actions/setup-bun/action.yml
vendored
@@ -11,10 +11,25 @@ runs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Get baseline download URL
|
||||
id: bun-url
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_ARCH" = "X64" ]; then
|
||||
V=$(node -p "require('./package.json').packageManager.split('@')[1]")
|
||||
case "$RUNNER_OS" in
|
||||
macOS) OS=darwin ;;
|
||||
Linux) OS=linux ;;
|
||||
Windows) OS=windows ;;
|
||||
esac
|
||||
echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: package.json
|
||||
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }}
|
||||
bun-download-url: ${{ steps.bun-url.outputs.url }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
4
.github/workflows/beta.yml
vendored
4
.github/workflows/beta.yml
vendored
@@ -27,7 +27,11 @@ jobs:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Install OpenCode
|
||||
run: bun i -g opencode-ai
|
||||
|
||||
- name: Sync beta branch
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
run: bun script/beta.ts
|
||||
|
||||
9
.github/workflows/compliance-close.yml
vendored
9
.github/workflows/compliance-close.yml
vendored
@@ -65,6 +65,15 @@ jobs:
|
||||
body: closeMessage,
|
||||
});
|
||||
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: item.number,
|
||||
name: 'needs:compliance',
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
if (isPR) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
|
||||
8
.github/workflows/docs-locale-sync.yml
vendored
8
.github/workflows/docs-locale-sync.yml
vendored
@@ -65,9 +65,9 @@ jobs:
|
||||
"packages/web/src/content/docs/*/*.mdx": "allow",
|
||||
".opencode": "allow",
|
||||
".opencode/agent": "allow",
|
||||
".opencode/agent/glossary": "allow",
|
||||
".opencode/glossary": "allow",
|
||||
".opencode/agent/translator.md": "allow",
|
||||
".opencode/agent/glossary/*.md": "allow"
|
||||
".opencode/glossary/*.md": "allow"
|
||||
},
|
||||
"edit": {
|
||||
"*": "deny",
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
"glob": {
|
||||
"*": "deny",
|
||||
"packages/web/src/content/docs*": "allow",
|
||||
".opencode/agent/glossary*": "allow"
|
||||
".opencode/glossary*": "allow"
|
||||
},
|
||||
"task": {
|
||||
"*": "deny",
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
"read": {
|
||||
"*": "deny",
|
||||
".opencode/agent/translator.md": "allow",
|
||||
".opencode/agent/glossary/*.md": "allow"
|
||||
".opencode/glossary/*.md": "allow"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
.github/workflows/pr-standards.yml
vendored
12
.github/workflows/pr-standards.yml
vendored
@@ -108,11 +108,11 @@ jobs:
|
||||
|
||||
await removeLabel('needs:title');
|
||||
|
||||
// Step 2: Check for linked issue (skip for docs/refactor PRs)
|
||||
const skipIssueCheck = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
|
||||
// Step 2: Check for linked issue (skip for docs/refactor/feat PRs)
|
||||
const skipIssueCheck = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
|
||||
if (skipIssueCheck) {
|
||||
await removeLabel('needs:issue');
|
||||
console.log('Skipping issue check for docs/refactor PR');
|
||||
console.log('Skipping issue check for docs/refactor/feat PR');
|
||||
return;
|
||||
}
|
||||
const query = `
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
|
||||
const body = pr.body || '';
|
||||
const title = pr.title;
|
||||
const isDocsOrRefactor = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
|
||||
const isDocsRefactorOrFeat = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
|
||||
|
||||
const issues = [];
|
||||
|
||||
@@ -225,8 +225,8 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
// Check: issue reference (skip for docs/refactor)
|
||||
if (!isDocsOrRefactor && hasIssueSection) {
|
||||
// Check: issue reference (skip for docs/refactor/feat)
|
||||
if (!isDocsRefactorOrFeat && hasIssueSection) {
|
||||
const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const issueContent = issueMatch ? issueMatch[1].trim() : '';
|
||||
const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent);
|
||||
|
||||
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -8,8 +8,16 @@ on:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
unit:
|
||||
name: unit (linux)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
name: unit (${{ matrix.settings.name }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- name: linux
|
||||
host: blacksmith-4vcpu-ubuntu-2404
|
||||
- name: windows
|
||||
host: blacksmith-4vcpu-windows-2025
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
58
.github/workflows/vouch-check-issue.yml
vendored
58
.github/workflows/vouch-check-issue.yml
vendored
@@ -42,15 +42,17 @@ jobs:
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Parse the .td file for denounced users
|
||||
// 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;
|
||||
if (!trimmed.startsWith('-')) continue;
|
||||
|
||||
const rest = trimmed.slice(1).trim();
|
||||
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();
|
||||
@@ -65,32 +67,50 @@ jobs:
|
||||
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
||||
if (!username) continue;
|
||||
|
||||
denounced.set(username.toLowerCase(), reason);
|
||||
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) {
|
||||
core.info(`User ${author} is not denounced. Allowing issue.`);
|
||||
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 denounced — close the issue
|
||||
const body = 'This issue has been automatically closed.';
|
||||
// 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.createComment({
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
labels: ['Vouched'],
|
||||
});
|
||||
|
||||
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}`);
|
||||
core.info(`Added vouched label to issue #${issueNumber} from ${author}`);
|
||||
|
||||
55
.github/workflows/vouch-check-pr.yml
vendored
55
.github/workflows/vouch-check-pr.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
@@ -42,15 +43,17 @@ jobs:
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Parse the .td file for denounced users
|
||||
// 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;
|
||||
if (!trimmed.startsWith('-')) continue;
|
||||
|
||||
const rest = trimmed.slice(1).trim();
|
||||
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();
|
||||
@@ -65,29 +68,47 @@ jobs:
|
||||
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
||||
if (!username) continue;
|
||||
|
||||
denounced.set(username.toLowerCase(), reason);
|
||||
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) {
|
||||
core.info(`User ${author} is not denounced. Allowing PR.`);
|
||||
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 denounced — close the PR
|
||||
await github.rest.issues.createComment({
|
||||
// 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,
|
||||
body: 'This pull request has been automatically closed.',
|
||||
labels: ['Vouched'],
|
||||
});
|
||||
|
||||
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}`);
|
||||
core.info(`Added vouched label to PR #${prNumber} from ${author}`);
|
||||
|
||||
1
.github/workflows/vouch-manage-by-issue.yml
vendored
1
.github/workflows/vouch-manage-by-issue.yml
vendored
@@ -33,5 +33,6 @@ jobs:
|
||||
with:
|
||||
issue-id: ${{ github.event.issue.number }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
roles: admin,maintain
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
|
||||
@@ -13,7 +13,7 @@ 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/agent/glossary/<locale>.md` when available (for example, `zh-cn.md`).
|
||||
- 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).
|
||||
|
||||
|
||||
265
bun.lock
265
bun.lock
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -109,7 +109,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -136,7 +136,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -160,7 +160,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -184,7 +184,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -217,7 +217,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -246,7 +246,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -262,7 +262,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -304,8 +304,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||
"@opentui/core": "0.1.81",
|
||||
"@opentui/solid": "0.1.81",
|
||||
"@opentui/core": "0.1.84",
|
||||
"@opentui/solid": "0.1.84",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -342,6 +342,7 @@
|
||||
"ulid": "catalog:",
|
||||
"vscode-jsonrpc": "8.2.1",
|
||||
"web-tree-sitter": "0.25.10",
|
||||
"which": "6.0.1",
|
||||
"xdg-basedir": "5.1.0",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "catalog:",
|
||||
@@ -364,6 +365,7 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/which": "3.0.4",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "1.0.0-beta.12-a5629fb",
|
||||
@@ -376,7 +378,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -396,7 +398,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -407,7 +409,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -418,9 +420,30 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@storybook/addon-a11y": "^10.2.10",
|
||||
"@storybook/addon-docs": "^10.2.10",
|
||||
"@storybook/addon-links": "^10.2.10",
|
||||
"@storybook/addon-onboarding": "^10.2.10",
|
||||
"@storybook/addon-vitest": "^10.2.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "18.0.25",
|
||||
"react": "18.2.0",
|
||||
"solid-js": "catalog:",
|
||||
"storybook": "^10.2.10",
|
||||
"storybook-solidjs-vite": "^10.0.9",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -462,7 +485,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -473,7 +496,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1136,6 +1159,8 @@
|
||||
|
||||
"@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="],
|
||||
|
||||
"@joshwooding/vite-plugin-react-docgen-typescript": ["@joshwooding/vite-plugin-react-docgen-typescript@0.6.4", "", { "dependencies": { "glob": "^13.0.1", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "typescript": ">= 4.3.x", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-6PyZBYKnnVNqOSB0YFly+62R7dmov8segT27A+RVTBVd4iAE6kbW9QBJGlyR2yG4D4ohzhZSTIu7BK1UTtmFFA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
@@ -1208,6 +1233,8 @@
|
||||
|
||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
||||
|
||||
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
|
||||
|
||||
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="],
|
||||
@@ -1302,6 +1329,8 @@
|
||||
|
||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||
|
||||
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
|
||||
|
||||
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
||||
|
||||
"@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"],
|
||||
@@ -1314,21 +1343,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.81", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.81", "@opentui/core-darwin-x64": "0.1.81", "@opentui/core-linux-arm64": "0.1.81", "@opentui/core-linux-x64": "0.1.81", "@opentui/core-win32-arm64": "0.1.81", "@opentui/core-win32-x64": "0.1.81", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-ooFjkkQ80DDC4X5eLvH8dBcLAtWwGp9RTaWsaeWet3GOv4N0SDcN8mi1XGhYnUlTuxmofby5eQrPegjtWHODlA=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.84", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.84", "@opentui/core-darwin-x64": "0.1.84", "@opentui/core-linux-arm64": "0.1.84", "@opentui/core-linux-x64": "0.1.84", "@opentui/core-win32-arm64": "0.1.84", "@opentui/core-win32-x64": "0.1.84", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-UdPD/sldUSiIu588l45lQq6q09zLW8L4GOgkA4E3fyExIlIgOrpIhFSKqZwbVCbe53dQOybHVUdKob64Xxhm4w=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.81", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I3Ry5JbkSQXs2g1me8yYr0v3CUcIIfLHzbWz9WMFla8kQDSa+HOr8IpZbqZDeIFgOVzolAXBmZhg0VJI3bZ7MA=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.84", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MpLdRYd2zLJP7IcWHu1SYXFet6ZfOzTUW12iaexlUL4DkAU+P5mO1v/OdLlZrmXVj/FD3BqDM5SUIXNE0+A46Q=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.81", "", { "os": "darwin", "cpu": "x64" }, "sha512-CrtNKu41D6+bOQdUOmDX4Q3hTL6p+sT55wugPzbDq7cdqFZabCeguBAyOlvRl2g2aJ93kmOWW6MXG0bPPklEFg=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.84", "", { "os": "darwin", "cpu": "x64" }, "sha512-eEFzEdo/WVVjbpwY2pnQBxkpqsWyGLHNv3kFc0RHvZ3ARIW2qb/Jqo9O9uUbR6th7H77V1NRJ0a6gvby48Oofg=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.81", "", { "os": "linux", "cpu": "arm64" }, "sha512-FJw9zmJop9WiMvtT07nSrfBLPLqskxL6xfV3GNft0mSYV+C3hdJ0qkiczGSHUX/6V7fmouM84RWwmY53Rb6hYQ=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.84", "", { "os": "linux", "cpu": "arm64" }, "sha512-AYfG9iLWavfVYZYbNfEZkCimqk8Uq8EzvHKwRkbP24XVZO7eALztZ6PWl2nJqDrw+rYBVSJg0uS6ON+M7NxyBA=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.81", "", { "os": "linux", "cpu": "x64" }, "sha512-Rj2AFIiuWI0BEMIvh/Jeuxty9Gp5ZhLuQU7ZHJJhojKo/mpBpMs9X+5kwZPZya/tyR8uVDAVyB6AOLkhdRW5lw=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.84", "", { "os": "linux", "cpu": "x64" }, "sha512-pLi3a3rqs1BMeCqj1GZm/Qi3zdwilQxaPEI/IeWc5qdzWInpGPHs3uadPJBAllEGRzvTmStMWI6iY6bdc98bng=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.81", "", { "os": "win32", "cpu": "arm64" }, "sha512-AiZB+mZ1cVr8plAPrPT98e3kw6D0OdOSe2CQYLgJRbfRlPqq3jl26lHPzDb3ZO2OR0oVGRPJvXraus939mvoiQ=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.84", "", { "os": "win32", "cpu": "arm64" }, "sha512-dcQruw9VyYACxukoB+iv8SskQxl5MMeY73uqvQ17dsnM2sfukT+c2MSgbgYImCKKixtlGXLcb5Weo36lQqI5AQ=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.81", "", { "os": "win32", "cpu": "x64" }, "sha512-l8R2Ni1CR4eHi3DTmSkEL/EjHAtOZ/sndYs3VVw+Ej2esL3Mf0W7qSO5S0YNBanz2VXZhbkmM6ERm9keH8RD3w=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.84", "", { "os": "win32", "cpu": "x64" }, "sha512-KiCaIVtVnZYQza+vAW8TDbUMxBgd2thvjNOIJ03NmjMiiRnfmO55wA3Zh9vNuQ5PJEYI0awzXYFAZ7kaMsDQxA=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.81", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.81", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-QRjS0wPuIhBRdY8tpG3yprCM4ZnOxWWHTuaZ4hhia2wFZygf7Ome6EuZnLXmtuOQjkjCwu0if8Yik6toc6QylA=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.84", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.84", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-5koIQ9Nk5CU6/uVxjjWJ7jtFeSNkczbZoEzHLdUK/T2bqHSyuPSFBeliQ1hfOwrN2G5SKFqIB3U5+1EcHxDCCA=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1774,6 +1803,26 @@
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@storybook/addon-a11y": ["@storybook/addon-a11y@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-1S9pDXgvbHhBStGarCvfJ3/rfcaiAcQHRhuM3Nk4WGSIYtC1LCSRuzYdDYU0aNRpdCbCrUA7kUCbqvIE3tH+3Q=="],
|
||||
|
||||
"@storybook/addon-docs": ["@storybook/addon-docs@10.2.10", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.2.10", "@storybook/icons": "^2.0.1", "@storybook/react-dom-shim": "10.2.10", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-2wIYtdvZIzPbQ5194M5Igpy8faNbQ135nuO5ZaZ2VuttqGr+IJcGnDP42zYwbAsGs28G8ohpkbSgIzVyJWUhPQ=="],
|
||||
|
||||
"@storybook/addon-links": ["@storybook/addon-links@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.2.10" }, "optionalPeers": ["react"] }, "sha512-oo9Xx4/2OVJtptXKpqH4ySri7ZuBdiSOXlZVGejEfLa0Jeajlh/KIlREpGvzPPOqUVT7dSddWzBjJmJUyQC3ew=="],
|
||||
|
||||
"@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.2.10", "", { "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-DkzZQTXHp99SpHMIQ5plbbHcs4EWVzWhLXlW+icA8sBlKo5Bwj540YcOApKbqB0m/OzWprsznwN7Kv4vfvHu4w=="],
|
||||
|
||||
"@storybook/addon-vitest": ["@storybook/addon-vitest@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.2.10", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-U2oHw+Ar+Xd06wDTB74VlujhIIW89OHThpJjwgqgM6NWrOC/XLllJ53ILFDyREBkMwpBD7gJQIoQpLEcKBIEhw=="],
|
||||
|
||||
"@storybook/builder-vite": ["@storybook/builder-vite@10.2.10", "", { "dependencies": { "@storybook/csf-plugin": "10.2.10", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.10", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-Wd6CYL7LvRRNiXMz977x9u/qMm7nmMw/7Dow2BybQo+Xbfy1KhVjIoZ/gOiG515zpojSozctNrJUbM0+jH1jwg=="],
|
||||
|
||||
"@storybook/csf-plugin": ["@storybook/csf-plugin@10.2.10", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.2.10", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-aFvgaNDAnKMjuyhPK5ialT22pPqMN0XfPBNPeeNVPYztngkdKBa8WFqF/umDd47HxAjebq+vn6uId1xHyOHH3g=="],
|
||||
|
||||
"@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="],
|
||||
|
||||
"@storybook/icons": ["@storybook/icons@2.0.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg=="],
|
||||
|
||||
"@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.2.10", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.2.10" } }, "sha512-TmBrhyLHn8B8rvDHKk5uW5BqzO1M1T+fqFNWg88NIAJOoyX4Uc90FIJjDuN1OJmWKGwB5vLmPwaKBYsTe1yS+w=="],
|
||||
|
||||
"@stripe/stripe-js": ["@stripe/stripe-js@8.6.1", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="],
|
||||
@@ -1866,6 +1915,12 @@
|
||||
|
||||
"@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="],
|
||||
|
||||
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
||||
|
||||
"@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
|
||||
|
||||
"@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
|
||||
|
||||
"@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
@@ -1876,6 +1931,8 @@
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
@@ -1978,6 +2035,8 @@
|
||||
|
||||
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
||||
|
||||
"@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
|
||||
@@ -2010,7 +2069,7 @@
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
|
||||
|
||||
@@ -2020,7 +2079,7 @@
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
|
||||
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
|
||||
|
||||
@@ -2116,6 +2175,8 @@
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||
|
||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||
|
||||
"astro": ["astro@5.7.13", "", { "dependencies": { "@astrojs/compiler": "^2.11.0", "@astrojs/internal-helpers": "0.6.1", "@astrojs/markdown-remark": "6.3.1", "@astrojs/telemetry": "3.2.1", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-cRGq2llKOhV3XMcYwQpfBIUcssN6HEK5CRbcMxAfd9OcFhvWE7KUy50zLioAZVVl3AqgUTJoNTlmZfD2eG0G1w=="],
|
||||
@@ -2144,6 +2205,8 @@
|
||||
|
||||
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
|
||||
"axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="],
|
||||
|
||||
"axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
@@ -2258,7 +2321,7 @@
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
||||
|
||||
"chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="],
|
||||
|
||||
@@ -2274,6 +2337,8 @@
|
||||
|
||||
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
|
||||
|
||||
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
|
||||
|
||||
"cheerio": ["cheerio@1.0.0-rc.12", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "htmlparser2": "^8.0.1", "parse5": "^7.0.0", "parse5-htmlparser2-tree-adapter": "^7.0.0" } }, "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q=="],
|
||||
|
||||
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
|
||||
@@ -2368,6 +2433,8 @@
|
||||
|
||||
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||
|
||||
"css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
@@ -2388,6 +2455,8 @@
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
|
||||
@@ -2440,6 +2509,8 @@
|
||||
|
||||
"dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="],
|
||||
|
||||
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
@@ -2834,6 +2905,8 @@
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
|
||||
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
@@ -2942,7 +3015,7 @@
|
||||
|
||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
"isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
|
||||
|
||||
"isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
|
||||
|
||||
@@ -3074,6 +3147,8 @@
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||
|
||||
"lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
@@ -3084,6 +3159,8 @@
|
||||
|
||||
"luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
|
||||
|
||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="],
|
||||
@@ -3234,6 +3311,8 @@
|
||||
|
||||
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
|
||||
|
||||
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||
|
||||
"miniflare": ["miniflare@4.20251118.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251118.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-uLSAE/DvOm392fiaig4LOaatxLjM7xzIniFRG5Y3yF9IduOYLLK/pkCPQNCgKQH3ou0YJRHnTN+09LPfqYNTQQ=="],
|
||||
|
||||
"minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||
@@ -3426,6 +3505,8 @@
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
|
||||
|
||||
"peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="],
|
||||
|
||||
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
|
||||
@@ -3492,6 +3573,8 @@
|
||||
|
||||
"pretty": ["pretty@2.0.0", "", { "dependencies": { "condense-newlines": "^0.2.1", "extend-shallow": "^2.0.1", "js-beautify": "^1.6.12" } }, "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w=="],
|
||||
|
||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||
|
||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
@@ -3534,8 +3617,12 @@
|
||||
|
||||
"react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="],
|
||||
|
||||
"react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="],
|
||||
|
||||
"react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
||||
|
||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="],
|
||||
@@ -3560,6 +3647,8 @@
|
||||
|
||||
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||
|
||||
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||
|
||||
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
||||
|
||||
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
|
||||
@@ -3568,6 +3657,8 @@
|
||||
|
||||
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
|
||||
|
||||
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
@@ -3760,7 +3851,7 @@
|
||||
|
||||
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
@@ -3808,6 +3899,10 @@
|
||||
|
||||
"stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="],
|
||||
|
||||
"storybook": ["storybook@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-N4U42qKgzMHS7DjqLz5bY4P7rnvJtYkWFCyKspZr3FhPUuy6CWOae3aYC2BjXkHrdug0Jyta6VxFTuB1tYUKhg=="],
|
||||
|
||||
"storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.9", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.1", "@storybook/builder-vite": "^10.0.0", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.8" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-n6MwWCL9mK/qIaUutE9vhGB0X1I1hVnKin2NL+iVC5oXfAiuaABVZlr/1oEeEypsgCdyDOcbEbhJmDWmaqGpPw=="],
|
||||
|
||||
"stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="],
|
||||
|
||||
"streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
|
||||
@@ -3834,6 +3929,8 @@
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||
|
||||
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
||||
|
||||
"stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="],
|
||||
|
||||
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
|
||||
@@ -3896,6 +3993,8 @@
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||
|
||||
"tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
|
||||
|
||||
"titleize": ["titleize@4.0.0", "", {}, "sha512-ZgUJ1K83rhdu7uh7EHAC2BgY5DzoX8V5rTvoWI4vFysggi6YjLe5gUXABPWAU7VkvGP7P/0YiWq+dcPeYDsf1g=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
@@ -3920,6 +4019,8 @@
|
||||
|
||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||
|
||||
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
|
||||
|
||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||
|
||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||
@@ -4020,6 +4121,8 @@
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
||||
|
||||
"unstorage": ["unstorage@2.0.0-alpha.5", "", { "peerDependencies": { "@azure/app-configuration": "^1.9.0", "@azure/cosmos": "^4.7.0", "@azure/data-tables": "^13.3.1", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.29.1", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.12.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.35.6", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.8.2", "lru-cache": "^11.2.2", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-Sj8btci21Twnd6M+N+MHhjg3fVn6lAPElPmvFTe0Y/wR0WImErUdA1PzlAaUavHylJ7uDiFwlZDQKm0elG4b7g=="],
|
||||
|
||||
"unzip-stream": ["unzip-stream@0.3.4", "", { "dependencies": { "binary": "^0.3.0", "mkdirp": "^0.5.1" } }, "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw=="],
|
||||
@@ -4032,6 +4135,8 @@
|
||||
|
||||
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="],
|
||||
|
||||
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||
@@ -4106,11 +4211,13 @@
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
"which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
|
||||
@@ -4260,6 +4367,8 @@
|
||||
|
||||
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.19.0", "smol-toml": "^1.5.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A=="],
|
||||
|
||||
"@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"@astrojs/sitemap/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@astrojs/solid-js/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||
@@ -4488,6 +4597,8 @@
|
||||
|
||||
"@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
|
||||
|
||||
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
@@ -4632,8 +4743,18 @@
|
||||
|
||||
"@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
||||
|
||||
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||
|
||||
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
|
||||
|
||||
"@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
||||
|
||||
"@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
|
||||
|
||||
"@vitest/mocker/@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
|
||||
|
||||
"@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
|
||||
|
||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
@@ -4692,12 +4813,12 @@
|
||||
|
||||
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"condense-newlines/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
|
||||
@@ -4714,6 +4835,8 @@
|
||||
|
||||
"esbuild-plugin-copy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
@@ -4814,6 +4937,10 @@
|
||||
|
||||
"postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
@@ -4842,12 +4969,16 @@
|
||||
|
||||
"sitemap/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
|
||||
|
||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="],
|
||||
|
||||
"sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
|
||||
|
||||
"storybook/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||
|
||||
"storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
"storybook/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
@@ -4880,6 +5011,10 @@
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
|
||||
"vitest/@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
||||
|
||||
"vitest/@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
|
||||
|
||||
"vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
|
||||
@@ -5210,6 +5345,8 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="],
|
||||
@@ -5254,6 +5391,8 @@
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"editorconfig/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
@@ -5304,6 +5443,60 @@
|
||||
|
||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||
|
||||
"storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
@@ -5372,6 +5565,8 @@
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="],
|
||||
|
||||
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Context as GitHubContext } from "@actions/github/lib/context"
|
||||
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
import { spawn } from "node:child_process"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
|
||||
type GitHubAuthor = {
|
||||
login: string
|
||||
@@ -281,7 +282,7 @@ async function assertOpencodeConnected() {
|
||||
connected = true
|
||||
break
|
||||
} catch (e) {}
|
||||
await Bun.sleep(300)
|
||||
await sleep(300)
|
||||
} while (retry++ < 30)
|
||||
|
||||
if (!connected) {
|
||||
|
||||
@@ -101,7 +101,7 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
|
||||
})
|
||||
|
||||
const zenLiteProduct = new stripe.Product("ZenLite", {
|
||||
name: "OpenCode Lite",
|
||||
name: "OpenCode Go",
|
||||
})
|
||||
const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
||||
product: zenLiteProduct.id,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-3hfy6nfEnGq4J6inH0pXANw05oas+81iuayn7J0pj9c=",
|
||||
"aarch64-linux": "sha256-dxWaLtzSeI5NfHwB6u0K10yxoA0ESz/r+zTEQ3FdKFY=",
|
||||
"aarch64-darwin": "sha256-kkK4rj4g0j2jJFXVmVH7CJcXlI8Dj/KmL/VC3iE4Z+8=",
|
||||
"x86_64-darwin": "sha256-jt51irxZd48kb0BItd8InP7lfsELUh0unVYO2es+a98="
|
||||
"x86_64-linux": "sha256-8erp2XB7iEkJtGqx+DqS7ZK4KHZwYrj5J/oW82fMn2g=",
|
||||
"aarch64-linux": "sha256-gWZnxBIZyKVLZweH9q844LtSDvd2m2d60MmINg53mPs=",
|
||||
"aarch64-darwin": "sha256-FVP2Q0ZEK1jEPuC5e4quOsxqpCTXO6W2KQdDGpIl1dc=",
|
||||
"x86_64-darwin": "sha256-uYdKJwWuj7nZSRXQjsZZsubV2zvYSTg+LOCFkbT4bkA="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.9",
|
||||
"packageManager": "bun@1.3.10",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||
|
||||
@@ -43,7 +43,7 @@ test("file tree can expand folders and open a file", async ({ page, gotoSession
|
||||
await tab.click()
|
||||
await expect(tab).toHaveAttribute("aria-selected", "true")
|
||||
|
||||
const code = page.locator('[data-component="code"]').first()
|
||||
await expect(code).toBeVisible()
|
||||
await expect(code).toContainText("export default function FileTree")
|
||||
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||
await expect(viewer).toBeVisible()
|
||||
await expect(viewer).toContainText("export default function FileTree")
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { modKey } from "../utils"
|
||||
|
||||
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
@@ -43,7 +44,60 @@ test("smoke file viewer renders real file content", async ({ page, gotoSession }
|
||||
await expect(tab).toBeVisible()
|
||||
await tab.click()
|
||||
|
||||
const code = page.locator('[data-component="code"]').first()
|
||||
await expect(code).toBeVisible()
|
||||
await expect(code.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible()
|
||||
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||
await expect(viewer).toBeVisible()
|
||||
await expect(viewer.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible()
|
||||
})
|
||||
|
||||
test("cmd+f opens text viewer search while prompt is focused", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type("/open")
|
||||
|
||||
const command = page.locator('[data-slash-id="file.open"]').first()
|
||||
await expect(command).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const dialog = page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: page.getByPlaceholder(/search files/i) })
|
||||
.first()
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const input = dialog.getByRole("textbox").first()
|
||||
await input.fill("package.json")
|
||||
|
||||
const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
|
||||
let index = -1
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
|
||||
index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
|
||||
return index >= 0
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const item = items.nth(index)
|
||||
await expect(item).toBeVisible()
|
||||
await item.click()
|
||||
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
const tab = page.getByRole("tab", { name: "package.json" })
|
||||
await expect(tab).toBeVisible()
|
||||
await tab.click()
|
||||
|
||||
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||
await expect(viewer).toBeVisible()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.press(`${modKey}+f`)
|
||||
|
||||
const findInput = page.getByPlaceholder("Find")
|
||||
await expect(findInput).toBeVisible()
|
||||
await expect(findInput).toBeFocused()
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
sessionIDFromUrl,
|
||||
} from "../actions"
|
||||
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||
import { createSdk, dirSlug } from "../utils"
|
||||
import { createSdk, dirSlug, sessionPath } from "../utils"
|
||||
|
||||
function slugFromUrl(url: string) {
|
||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||
@@ -51,7 +51,6 @@ test("switching back to a project opens the latest workspace session", async ({
|
||||
|
||||
const other = await createTestProject()
|
||||
const otherSlug = dirSlug(other)
|
||||
const stamp = Date.now()
|
||||
let rootDir: string | undefined
|
||||
let workspaceDir: string | undefined
|
||||
let sessionID: string | undefined
|
||||
@@ -80,6 +79,7 @@ test("switching back to a project opens the latest workspace session", async ({
|
||||
|
||||
const workspaceSlug = slugFromUrl(page.url())
|
||||
workspaceDir = base64Decode(workspaceSlug)
|
||||
if (!workspaceDir) throw new Error(`Failed to decode workspace slug: ${workspaceSlug}`)
|
||||
await openSidebar(page)
|
||||
|
||||
const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first()
|
||||
@@ -92,15 +92,14 @@ test("switching back to a project opens the latest workspace session", async ({
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`))
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await expect(prompt).toBeVisible()
|
||||
await prompt.fill(`project switch remembers workspace ${stamp}`)
|
||||
await prompt.press("Enter")
|
||||
|
||||
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
|
||||
const created = sessionIDFromUrl(page.url())
|
||||
if (!created) throw new Error(`Failed to parse session id from URL: ${page.url()}`)
|
||||
const created = await createSdk(workspaceDir)
|
||||
.session.create()
|
||||
.then((x) => x.data?.id)
|
||||
if (!created) throw new Error(`Failed to create session for workspace: ${workspaceDir}`)
|
||||
sessionID = created
|
||||
|
||||
await page.goto(sessionPath(workspaceDir, created))
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
|
||||
|
||||
await openSidebar(page)
|
||||
@@ -114,7 +113,8 @@ test("switching back to a project opens the latest workspace session", async ({
|
||||
await expect(rootButton).toBeVisible()
|
||||
await rootButton.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
|
||||
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created)
|
||||
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
|
||||
},
|
||||
{ extra: [other] },
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { clearSessionDockSeed, seedSessionPermission, seedSessionQuestion, seedSessionTodos } from "../actions"
|
||||
import { clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions"
|
||||
import {
|
||||
permissionDockSelector,
|
||||
promptSelector,
|
||||
@@ -11,11 +11,23 @@ import {
|
||||
} from "../selectors"
|
||||
|
||||
type Sdk = Parameters<typeof clearSessionDockSeed>[0]
|
||||
type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" }
|
||||
|
||||
async function withDockSession<T>(sdk: Sdk, title: string, fn: (session: { id: string; title: string }) => Promise<T>) {
|
||||
const session = await sdk.session.create({ title }).then((r) => r.data)
|
||||
async function withDockSession<T>(
|
||||
sdk: Sdk,
|
||||
title: string,
|
||||
fn: (session: { id: string; title: string }) => Promise<T>,
|
||||
opts?: { permission?: PermissionRule[] },
|
||||
) {
|
||||
const session = await sdk.session
|
||||
.create(opts?.permission ? { title, permission: opts.permission } : { title })
|
||||
.then((r) => r.data)
|
||||
if (!session?.id) throw new Error("Session create did not return an id")
|
||||
return fn(session)
|
||||
try {
|
||||
return await fn(session)
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
test.setTimeout(120_000)
|
||||
@@ -28,6 +40,85 @@ async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>
|
||||
}
|
||||
}
|
||||
|
||||
async function clearPermissionDock(page: any, label: RegExp) {
|
||||
const dock = page.locator(permissionDockSelector)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const count = await dock.count()
|
||||
if (count === 0) return
|
||||
await dock.getByRole("button", { name: label }).click()
|
||||
await page.waitForTimeout(150)
|
||||
}
|
||||
}
|
||||
|
||||
async function withMockPermission<T>(
|
||||
page: any,
|
||||
request: {
|
||||
id: string
|
||||
sessionID: string
|
||||
permission: string
|
||||
patterns: string[]
|
||||
metadata?: Record<string, unknown>
|
||||
always?: string[]
|
||||
},
|
||||
opts: { child?: any } | undefined,
|
||||
fn: () => Promise<T>,
|
||||
) {
|
||||
let pending = [
|
||||
{
|
||||
...request,
|
||||
always: request.always ?? ["*"],
|
||||
metadata: request.metadata ?? {},
|
||||
},
|
||||
]
|
||||
|
||||
const list = async (route: any) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(pending),
|
||||
})
|
||||
}
|
||||
|
||||
const reply = async (route: any) => {
|
||||
const url = new URL(route.request().url())
|
||||
const id = url.pathname.split("/").pop()
|
||||
pending = pending.filter((item) => item.id !== id)
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(true),
|
||||
})
|
||||
}
|
||||
|
||||
await page.route("**/permission", list)
|
||||
await page.route("**/session/*/permissions/*", reply)
|
||||
|
||||
const sessionList = opts?.child
|
||||
? async (route: any) => {
|
||||
const res = await route.fetch()
|
||||
const json = await res.json()
|
||||
const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined
|
||||
if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child)
|
||||
await route.fulfill({
|
||||
status: res.status(),
|
||||
headers: res.headers(),
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(json),
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
|
||||
if (sessionList) await page.route("**/session?*", sessionList)
|
||||
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
await page.unroute("**/permission", list)
|
||||
await page.unroute("**/session/*/permissions/*", reply)
|
||||
if (sessionList) await page.unroute("**/session?*", sessionList)
|
||||
}
|
||||
}
|
||||
|
||||
test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => {
|
||||
await withDockSession(sdk, "e2e composer dock default", async (session) => {
|
||||
await gotoSession(session.id)
|
||||
@@ -76,72 +167,175 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess
|
||||
|
||||
test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => {
|
||||
await withDockSession(sdk, "e2e composer dock permission once", async (session) => {
|
||||
await withDockSeed(sdk, session.id, async () => {
|
||||
await gotoSession(session.id)
|
||||
|
||||
await seedSessionPermission(sdk, {
|
||||
await gotoSession(session.id)
|
||||
await withMockPermission(
|
||||
page,
|
||||
{
|
||||
id: "per_e2e_once",
|
||||
sessionID: session.id,
|
||||
permission: "bash",
|
||||
patterns: ["README.md"],
|
||||
description: "Need permission for command",
|
||||
})
|
||||
patterns: ["/tmp/opencode-e2e-perm-once"],
|
||||
metadata: { description: "Need permission for command" },
|
||||
},
|
||||
undefined,
|
||||
async () => {
|
||||
await page.goto(page.url())
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
|
||||
await page
|
||||
.locator(permissionDockSelector)
|
||||
.getByRole("button", { name: /allow once/i })
|
||||
.click()
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
})
|
||||
await clearPermissionDock(page, /allow once/i)
|
||||
await page.goto(page.url())
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => {
|
||||
await withDockSession(sdk, "e2e composer dock permission reject", async (session) => {
|
||||
await withDockSeed(sdk, session.id, async () => {
|
||||
await gotoSession(session.id)
|
||||
|
||||
await seedSessionPermission(sdk, {
|
||||
await gotoSession(session.id)
|
||||
await withMockPermission(
|
||||
page,
|
||||
{
|
||||
id: "per_e2e_reject",
|
||||
sessionID: session.id,
|
||||
permission: "bash",
|
||||
patterns: ["REJECT.md"],
|
||||
})
|
||||
patterns: ["/tmp/opencode-e2e-perm-reject"],
|
||||
},
|
||||
undefined,
|
||||
async () => {
|
||||
await page.goto(page.url())
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
|
||||
await page.locator(permissionDockSelector).getByRole("button", { name: /deny/i }).click()
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
})
|
||||
await clearPermissionDock(page, /deny/i)
|
||||
await page.goto(page.url())
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => {
|
||||
await withDockSession(sdk, "e2e composer dock permission always", async (session) => {
|
||||
await withDockSeed(sdk, session.id, async () => {
|
||||
await gotoSession(session.id)
|
||||
|
||||
await seedSessionPermission(sdk, {
|
||||
await gotoSession(session.id)
|
||||
await withMockPermission(
|
||||
page,
|
||||
{
|
||||
id: "per_e2e_always",
|
||||
sessionID: session.id,
|
||||
permission: "bash",
|
||||
patterns: ["README.md"],
|
||||
description: "Need permission for command",
|
||||
patterns: ["/tmp/opencode-e2e-perm-always"],
|
||||
metadata: { description: "Need permission for command" },
|
||||
},
|
||||
undefined,
|
||||
async () => {
|
||||
await page.goto(page.url())
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
|
||||
await clearPermissionDock(page, /allow always/i)
|
||||
await page.goto(page.url())
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("child session question request blocks parent dock and unblocks after submit", async ({
|
||||
page,
|
||||
sdk,
|
||||
gotoSession,
|
||||
}) => {
|
||||
await withDockSession(sdk, "e2e composer dock child question parent", async (session) => {
|
||||
await gotoSession(session.id)
|
||||
|
||||
const child = await sdk.session
|
||||
.create({
|
||||
title: "e2e composer dock child question",
|
||||
parentID: session.id,
|
||||
})
|
||||
.then((r) => r.data)
|
||||
if (!child?.id) throw new Error("Child session create did not return an id")
|
||||
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
try {
|
||||
await withDockSeed(sdk, child.id, async () => {
|
||||
await seedSessionQuestion(sdk, {
|
||||
sessionID: child.id,
|
||||
questions: [
|
||||
{
|
||||
header: "Child input",
|
||||
question: "Pick one child option",
|
||||
options: [
|
||||
{ label: "Continue", description: "Continue child" },
|
||||
{ label: "Stop", description: "Stop child" },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await page
|
||||
.locator(permissionDockSelector)
|
||||
.getByRole("button", { name: /allow always/i })
|
||||
.click()
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
})
|
||||
const dock = page.locator(questionDockSelector)
|
||||
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
|
||||
await dock.locator('[data-slot="question-option"]').first().click()
|
||||
await dock.getByRole("button", { name: /submit/i }).click()
|
||||
|
||||
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
})
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: child.id }).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("child session permission request blocks parent dock and supports allow once", async ({
|
||||
page,
|
||||
sdk,
|
||||
gotoSession,
|
||||
}) => {
|
||||
await withDockSession(sdk, "e2e composer dock child permission parent", async (session) => {
|
||||
await gotoSession(session.id)
|
||||
|
||||
const child = await sdk.session
|
||||
.create({
|
||||
title: "e2e composer dock child permission",
|
||||
parentID: session.id,
|
||||
})
|
||||
.then((r) => r.data)
|
||||
if (!child?.id) throw new Error("Child session create did not return an id")
|
||||
|
||||
try {
|
||||
await withMockPermission(
|
||||
page,
|
||||
{
|
||||
id: "per_e2e_child",
|
||||
sessionID: child.id,
|
||||
permission: "bash",
|
||||
patterns: ["/tmp/opencode-e2e-perm-child"],
|
||||
metadata: { description: "Need child permission" },
|
||||
},
|
||||
{ child },
|
||||
async () => {
|
||||
await page.goto(page.url())
|
||||
const dock = page.locator(permissionDockSelector)
|
||||
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
|
||||
await clearPermissionDock(page, /allow once/i)
|
||||
await page.goto(page.url())
|
||||
|
||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: child.id }).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
|
||||
export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost"
|
||||
export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"
|
||||
export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||
|
||||
export const serverUrl = `http://${serverHost}:${serverPort}`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.15",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig, devices } from "@playwright/test"
|
||||
|
||||
const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000)
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${port}`
|
||||
const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost"
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`
|
||||
const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"
|
||||
const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||
const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
|
||||
const reuse = !process.env.CI
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import "@/index.css"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { File } from "@opencode-ai/ui/file"
|
||||
import { I18nProvider } from "@opencode-ai/ui/context"
|
||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||
import { Diff } from "@opencode-ai/ui/diff"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
@@ -122,9 +120,7 @@ export function AppBaseProviders(props: ParentProps) {
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<DialogProvider>
|
||||
<MarkedProviderWithNativeParser>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||
</MarkedProviderWithNativeParser>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -97,9 +97,20 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
<div class="w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
|
||||
</Show>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
<Show when={i.id === "opencode-go"}>
|
||||
<>
|
||||
<div class="text-14-regular text-text-weak">
|
||||
{language.t("dialog.provider.opencodeGo.tagline")}
|
||||
</div>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</>
|
||||
</Show>
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
||||
</Show>
|
||||
|
||||
@@ -29,6 +29,7 @@ export const DialogSelectProvider: Component = () => {
|
||||
if (id === "anthropic") return language.t("dialog.provider.anthropic.note")
|
||||
if (id === "openai") return language.t("dialog.provider.openai.note")
|
||||
if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note")
|
||||
if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline")
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -70,6 +71,9 @@ export const DialogSelectProvider: Component = () => {
|
||||
<div class="px-1.25 w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={icon(i.id)} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
|
||||
</Show>
|
||||
<Show when={i.id === CUSTOM_ID}>
|
||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||
</Show>
|
||||
@@ -77,6 +81,9 @@ export const DialogSelectProvider: Component = () => {
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
<Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show>
|
||||
<Show when={i.id === "opencode-go"}>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
@@ -9,32 +10,27 @@ import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { ServerRow } from "@/components/server/server-row"
|
||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||
|
||||
interface AddRowProps {
|
||||
value: string
|
||||
placeholder: string
|
||||
adding: boolean
|
||||
error: string
|
||||
status: boolean | undefined
|
||||
onChange: (value: string) => void
|
||||
onKeyDown: (event: KeyboardEvent) => void
|
||||
onBlur: () => void
|
||||
}
|
||||
|
||||
interface EditRowProps {
|
||||
interface ServerFormProps {
|
||||
value: string
|
||||
name: string
|
||||
username: string
|
||||
password: string
|
||||
placeholder: string
|
||||
busy: boolean
|
||||
error: string
|
||||
status: boolean | undefined
|
||||
onChange: (value: string) => void
|
||||
onKeyDown: (event: KeyboardEvent) => void
|
||||
onBlur: () => void
|
||||
onNameChange: (value: string) => void
|
||||
onUsernameChange: (value: string) => void
|
||||
onPasswordChange: (value: string) => void
|
||||
onSubmit: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown) {
|
||||
@@ -83,83 +79,86 @@ function useServerPreview(fetcher: typeof fetch) {
|
||||
return host.includes(".") || host.includes(":")
|
||||
}
|
||||
|
||||
const previewStatus = async (value: string, setStatus: (value: boolean | undefined) => void) => {
|
||||
const previewStatus = async (
|
||||
value: string,
|
||||
username: string,
|
||||
password: string,
|
||||
setStatus: (value: boolean | undefined) => void,
|
||||
) => {
|
||||
setStatus(undefined)
|
||||
if (!looksComplete(value)) return
|
||||
const normalized = normalizeServerUrl(value)
|
||||
if (!normalized) return
|
||||
const result = await checkServerHealth({ url: normalized }, fetcher)
|
||||
const http: ServerConnection.HttpBase = { url: normalized }
|
||||
if (username) http.username = username
|
||||
if (password) http.password = password
|
||||
const result = await checkServerHealth(http, fetcher)
|
||||
setStatus(result.healthy)
|
||||
}
|
||||
|
||||
return { previewStatus }
|
||||
}
|
||||
|
||||
function AddRow(props: AddRowProps) {
|
||||
return (
|
||||
<div class="flex items-center px-4 min-h-14 py-3 min-w-0 flex-1">
|
||||
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full absolute left-3 top-1/2 -translate-y-1/2 z-10 pointer-events-none": true,
|
||||
"bg-icon-success-base": props.status === true,
|
||||
"bg-icon-critical-base": props.status === false,
|
||||
"bg-border-weak-base": props.status === undefined,
|
||||
}}
|
||||
ref={(el) => {
|
||||
// Position relative to input-wrapper
|
||||
requestAnimationFrame(() => {
|
||||
const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]')
|
||||
if (wrapper instanceof HTMLElement) {
|
||||
wrapper.appendChild(el)
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="text"
|
||||
hideLabel
|
||||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
autofocus
|
||||
validationState={props.error ? "invalid" : "valid"}
|
||||
error={props.error}
|
||||
disabled={props.adding}
|
||||
onChange={props.onChange}
|
||||
onKeyDown={props.onKeyDown}
|
||||
onBlur={props.onBlur}
|
||||
class="pl-7"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function ServerForm(props: ServerFormProps) {
|
||||
const language = useLanguage()
|
||||
const keyDown = (event: KeyboardEvent) => {
|
||||
event.stopPropagation()
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
props.onBack()
|
||||
return
|
||||
}
|
||||
if (event.key !== "Enter" || event.isComposing) return
|
||||
event.preventDefault()
|
||||
props.onSubmit()
|
||||
}
|
||||
|
||||
function EditRow(props: EditRowProps) {
|
||||
return (
|
||||
<div class="flex items-center gap-3 px-4 min-w-0 flex-1" onClick={(event) => event.stopPropagation()}>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full shrink-0": true,
|
||||
"bg-icon-success-base": props.status === true,
|
||||
"bg-icon-critical-base": props.status === false,
|
||||
"bg-border-weak-base": props.status === undefined,
|
||||
}}
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="px-5">
|
||||
<div class="bg-surface-raised-base rounded-md p-5 flex flex-col gap-3">
|
||||
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
||||
<TextField
|
||||
type="text"
|
||||
label={language.t("dialog.server.add.url")}
|
||||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
autofocus
|
||||
validationState={props.error ? "invalid" : "valid"}
|
||||
error={props.error}
|
||||
disabled={props.busy}
|
||||
onChange={props.onChange}
|
||||
onKeyDown={keyDown}
|
||||
/>
|
||||
</div>
|
||||
<TextField
|
||||
type="text"
|
||||
hideLabel
|
||||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
autofocus
|
||||
validationState={props.error ? "invalid" : "valid"}
|
||||
error={props.error}
|
||||
label={language.t("dialog.server.add.name")}
|
||||
placeholder={language.t("dialog.server.add.namePlaceholder")}
|
||||
value={props.name}
|
||||
disabled={props.busy}
|
||||
onChange={props.onChange}
|
||||
onKeyDown={props.onKeyDown}
|
||||
onBlur={props.onBlur}
|
||||
onChange={props.onNameChange}
|
||||
onKeyDown={keyDown}
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-2 min-w-0">
|
||||
<TextField
|
||||
type="text"
|
||||
label={language.t("dialog.server.add.username")}
|
||||
placeholder="username"
|
||||
value={props.username}
|
||||
disabled={props.busy}
|
||||
onChange={props.onUsernameChange}
|
||||
onKeyDown={keyDown}
|
||||
/>
|
||||
<TextField
|
||||
type="password"
|
||||
label={language.t("dialog.server.add.password")}
|
||||
placeholder="password"
|
||||
value={props.password}
|
||||
disabled={props.busy}
|
||||
onChange={props.onPasswordChange}
|
||||
onKeyDown={keyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -174,11 +173,13 @@ export function DialogSelectServer() {
|
||||
const fetcher = platform.fetch ?? globalThis.fetch
|
||||
const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language)
|
||||
const { previewStatus } = useServerPreview(fetcher)
|
||||
let listRoot: HTMLDivElement | undefined
|
||||
const [store, setStore] = createStore({
|
||||
status: {} as Record<ServerConnection.Key, ServerHealth | undefined>,
|
||||
addServer: {
|
||||
url: "",
|
||||
name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
adding: false,
|
||||
error: "",
|
||||
showForm: false,
|
||||
@@ -187,6 +188,9 @@ export function DialogSelectServer() {
|
||||
editServer: {
|
||||
id: undefined as string | undefined,
|
||||
value: "",
|
||||
name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
error: "",
|
||||
busy: false,
|
||||
status: undefined as boolean | undefined,
|
||||
@@ -196,27 +200,32 @@ export function DialogSelectServer() {
|
||||
const resetAdd = () => {
|
||||
setStore("addServer", {
|
||||
url: "",
|
||||
name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
adding: false,
|
||||
error: "",
|
||||
showForm: false,
|
||||
status: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const resetEdit = () => {
|
||||
setStore("editServer", {
|
||||
id: undefined,
|
||||
value: "",
|
||||
name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
error: "",
|
||||
status: undefined,
|
||||
busy: false,
|
||||
})
|
||||
}
|
||||
|
||||
const replaceServer = (original: ServerConnection.Http, next: string) => {
|
||||
const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => {
|
||||
const active = server.key
|
||||
const newConn = server.add(next)
|
||||
if (!newConn) return
|
||||
|
||||
const nextActive = active === ServerConnection.key(original) ? ServerConnection.key(newConn) : active
|
||||
if (nextActive) server.setActive(nextActive)
|
||||
server.remove(ServerConnection.key(original))
|
||||
@@ -271,8 +280,8 @@ export function DialogSelectServer() {
|
||||
async function select(conn: ServerConnection.Any, persist?: boolean) {
|
||||
if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return
|
||||
dialog.close()
|
||||
if (persist) {
|
||||
server.add(conn.http.url)
|
||||
if (persist && conn.type === "http") {
|
||||
server.add(conn)
|
||||
navigate("/")
|
||||
return
|
||||
}
|
||||
@@ -283,21 +292,59 @@ export function DialogSelectServer() {
|
||||
const handleAddChange = (value: string) => {
|
||||
if (store.addServer.adding) return
|
||||
setStore("addServer", { url: value, error: "" })
|
||||
void previewStatus(value, (next) => setStore("addServer", { status: next }))
|
||||
void previewStatus(value, store.addServer.username, store.addServer.password, (next) =>
|
||||
setStore("addServer", { status: next }),
|
||||
)
|
||||
}
|
||||
|
||||
const scrollListToBottom = () => {
|
||||
const scroll = listRoot?.querySelector<HTMLDivElement>('[data-slot="list-scroll"]')
|
||||
if (!scroll) return
|
||||
requestAnimationFrame(() => {
|
||||
scroll.scrollTop = scroll.scrollHeight
|
||||
})
|
||||
const handleAddNameChange = (value: string) => {
|
||||
if (store.addServer.adding) return
|
||||
setStore("addServer", { name: value, error: "" })
|
||||
}
|
||||
|
||||
const handleAddUsernameChange = (value: string) => {
|
||||
if (store.addServer.adding) return
|
||||
setStore("addServer", { username: value, error: "" })
|
||||
void previewStatus(store.addServer.url, value, store.addServer.password, (next) =>
|
||||
setStore("addServer", { status: next }),
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddPasswordChange = (value: string) => {
|
||||
if (store.addServer.adding) return
|
||||
setStore("addServer", { password: value, error: "" })
|
||||
void previewStatus(store.addServer.url, store.addServer.username, value, (next) =>
|
||||
setStore("addServer", { status: next }),
|
||||
)
|
||||
}
|
||||
|
||||
const handleEditChange = (value: string) => {
|
||||
if (store.editServer.busy) return
|
||||
setStore("editServer", { value, error: "" })
|
||||
void previewStatus(value, (next) => setStore("editServer", { status: next }))
|
||||
void previewStatus(value, store.editServer.username, store.editServer.password, (next) =>
|
||||
setStore("editServer", { status: next }),
|
||||
)
|
||||
}
|
||||
|
||||
const handleEditNameChange = (value: string) => {
|
||||
if (store.editServer.busy) return
|
||||
setStore("editServer", { name: value, error: "" })
|
||||
}
|
||||
|
||||
const handleEditUsernameChange = (value: string) => {
|
||||
if (store.editServer.busy) return
|
||||
setStore("editServer", { username: value, error: "" })
|
||||
void previewStatus(store.editServer.value, value, store.editServer.password, (next) =>
|
||||
setStore("editServer", { status: next }),
|
||||
)
|
||||
}
|
||||
|
||||
const handleEditPasswordChange = (value: string) => {
|
||||
if (store.editServer.busy) return
|
||||
setStore("editServer", { password: value, error: "" })
|
||||
void previewStatus(store.editServer.value, store.editServer.username, value, (next) =>
|
||||
setStore("editServer", { status: next }),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleAdd(value: string) {
|
||||
@@ -310,16 +357,22 @@ export function DialogSelectServer() {
|
||||
|
||||
setStore("addServer", { adding: true, error: "" })
|
||||
|
||||
const result = await checkServerHealth({ url: normalized }, fetcher)
|
||||
const conn: ServerConnection.Http = {
|
||||
type: "http",
|
||||
http: { url: normalized },
|
||||
}
|
||||
if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
|
||||
if (store.addServer.username) conn.http.username = store.addServer.username
|
||||
if (store.addServer.password) conn.http.password = store.addServer.password
|
||||
const result = await checkServerHealth(conn.http, fetcher)
|
||||
setStore("addServer", { adding: false })
|
||||
|
||||
if (!result.healthy) {
|
||||
setStore("addServer", { error: language.t("dialog.server.add.error") })
|
||||
return
|
||||
}
|
||||
|
||||
resetAdd()
|
||||
await select({ type: "http", http: { url: normalized } }, true)
|
||||
await select(conn, true)
|
||||
}
|
||||
|
||||
async function handleEdit(original: ServerConnection.Any, value: string) {
|
||||
@@ -330,53 +383,115 @@ export function DialogSelectServer() {
|
||||
return
|
||||
}
|
||||
|
||||
if (normalized === original.http.url) {
|
||||
const name = store.editServer.name.trim() || undefined
|
||||
const username = store.editServer.username || undefined
|
||||
const password = store.editServer.password || undefined
|
||||
const existingName = original.displayName
|
||||
if (
|
||||
normalized === original.http.url &&
|
||||
name === existingName &&
|
||||
username === original.http.username &&
|
||||
password === original.http.password
|
||||
) {
|
||||
resetEdit()
|
||||
return
|
||||
}
|
||||
|
||||
setStore("editServer", { busy: true, error: "" })
|
||||
|
||||
const result = await checkServerHealth({ url: normalized }, fetcher)
|
||||
const conn: ServerConnection.Http = {
|
||||
type: "http",
|
||||
displayName: name,
|
||||
http: { url: normalized, username, password },
|
||||
}
|
||||
const result = await checkServerHealth(conn.http, fetcher)
|
||||
setStore("editServer", { busy: false })
|
||||
|
||||
if (!result.healthy) {
|
||||
setStore("editServer", { error: language.t("dialog.server.add.error") })
|
||||
return
|
||||
}
|
||||
|
||||
replaceServer(original, normalized)
|
||||
if (normalized === original.http.url) {
|
||||
server.add(conn)
|
||||
} else {
|
||||
replaceServer(original, conn)
|
||||
}
|
||||
|
||||
resetEdit()
|
||||
}
|
||||
|
||||
const handleAddKey = (event: KeyboardEvent) => {
|
||||
event.stopPropagation()
|
||||
if (event.key !== "Enter" || event.isComposing) return
|
||||
event.preventDefault()
|
||||
handleAdd(store.addServer.url)
|
||||
const mode = createMemo<"list" | "add" | "edit">(() => {
|
||||
if (store.editServer.id) return "edit"
|
||||
if (store.addServer.showForm) return "add"
|
||||
return "list"
|
||||
})
|
||||
|
||||
const editing = createMemo(() => {
|
||||
if (!store.editServer.id) return
|
||||
return items().find((x) => x.type === "http" && x.http.url === store.editServer.id)
|
||||
})
|
||||
|
||||
const resetForm = () => {
|
||||
resetAdd()
|
||||
resetEdit()
|
||||
}
|
||||
|
||||
const blurAdd = () => {
|
||||
if (!store.addServer.url.trim()) {
|
||||
resetAdd()
|
||||
return
|
||||
}
|
||||
handleAdd(store.addServer.url)
|
||||
const startAdd = () => {
|
||||
resetEdit()
|
||||
setStore("addServer", {
|
||||
showForm: true,
|
||||
url: "",
|
||||
name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
error: "",
|
||||
status: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const handleEditKey = (event: KeyboardEvent, original: ServerConnection.Any) => {
|
||||
event.stopPropagation()
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
resetEdit()
|
||||
const startEdit = (conn: ServerConnection.Http) => {
|
||||
resetAdd()
|
||||
setStore("editServer", {
|
||||
id: conn.http.url,
|
||||
value: conn.http.url,
|
||||
name: conn.displayName ?? "",
|
||||
username: conn.http.username ?? "",
|
||||
password: conn.http.password ?? "",
|
||||
error: "",
|
||||
status: store.status[ServerConnection.key(conn)]?.healthy,
|
||||
busy: false,
|
||||
})
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
if (mode() === "add") {
|
||||
void handleAdd(store.addServer.url)
|
||||
return
|
||||
}
|
||||
if (event.key !== "Enter" || event.isComposing) return
|
||||
event.preventDefault()
|
||||
handleEdit(original, store.editServer.value)
|
||||
const original = editing()
|
||||
if (!original) return
|
||||
void handleEdit(original, store.editServer.value)
|
||||
}
|
||||
|
||||
const isFormMode = createMemo(() => mode() !== "list")
|
||||
const isAddMode = createMemo(() => mode() === "add")
|
||||
const formBusy = createMemo(() => (isAddMode() ? store.addServer.adding : store.editServer.busy))
|
||||
|
||||
const formTitle = createMemo(() => {
|
||||
if (!isFormMode()) return language.t("dialog.server.title")
|
||||
return (
|
||||
<div class="flex items-center gap-2 -ml-2">
|
||||
<IconButton icon="arrow-left" variant="ghost" onClick={resetForm} aria-label={language.t("common.goBack")} />
|
||||
<span>{isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!store.editServer.id) return
|
||||
if (editing()) return
|
||||
resetEdit()
|
||||
})
|
||||
|
||||
async function handleRemove(url: ServerConnection.Key) {
|
||||
server.remove(url)
|
||||
if ((await platform.getDefaultServerUrl?.()) === url) {
|
||||
@@ -385,9 +500,29 @@ export function DialogSelectServer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={language.t("dialog.server.title")}>
|
||||
<Dialog title={formTitle()}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div ref={(el) => (listRoot = el)}>
|
||||
<Show
|
||||
when={!isFormMode()}
|
||||
fallback={
|
||||
<ServerForm
|
||||
value={isAddMode() ? store.addServer.url : store.editServer.value}
|
||||
name={isAddMode() ? store.addServer.name : store.editServer.name}
|
||||
username={isAddMode() ? store.addServer.username : store.editServer.username}
|
||||
password={isAddMode() ? store.addServer.password : store.editServer.password}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
busy={formBusy()}
|
||||
error={isAddMode() ? store.addServer.error : store.editServer.error}
|
||||
status={isAddMode() ? store.addServer.status : store.editServer.status}
|
||||
onChange={isAddMode() ? handleAddChange : handleEditChange}
|
||||
onNameChange={isAddMode() ? handleAddNameChange : handleEditNameChange}
|
||||
onUsernameChange={isAddMode() ? handleAddUsernameChange : handleEditUsernameChange}
|
||||
onPasswordChange={isAddMode() ? handleAddPasswordChange : handleEditPasswordChange}
|
||||
onSubmit={submitForm}
|
||||
onBack={resetForm}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<List
|
||||
search={{
|
||||
placeholder: language.t("dialog.server.search.placeholder"),
|
||||
@@ -400,143 +535,110 @@ export function DialogSelectServer() {
|
||||
onSelect={(x) => {
|
||||
if (x) select(x)
|
||||
}}
|
||||
onFilter={(value) => {
|
||||
if (value && store.addServer.showForm && !store.addServer.adding) {
|
||||
resetAdd()
|
||||
}
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0"
|
||||
add={
|
||||
store.addServer.showForm
|
||||
? {
|
||||
render: () => (
|
||||
<AddRow
|
||||
value={store.addServer.url}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
adding={store.addServer.adding}
|
||||
error={store.addServer.error}
|
||||
status={store.addServer.status}
|
||||
onChange={handleAddChange}
|
||||
onKeyDown={handleAddKey}
|
||||
onBlur={blurAdd}
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
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-raised-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) => {
|
||||
const key = ServerConnection.key(i)
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
||||
<Show
|
||||
when={store.editServer.id !== i.http.url}
|
||||
fallback={
|
||||
<EditRow
|
||||
value={store.editServer.value}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
busy={store.editServer.busy}
|
||||
error={store.editServer.error}
|
||||
status={store.editServer.status}
|
||||
onChange={handleEditChange}
|
||||
onKeyDown={(event) => handleEditKey(event, i)}
|
||||
onBlur={() => handleEdit(i, store.editServer.value)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
status={store.status[key]}
|
||||
dimmed={store.status[key]?.healthy === false}
|
||||
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultUrl() === i.http.url}>
|
||||
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={store.editServer.id !== i.http.url}>
|
||||
<div class="flex items-center justify-center gap-5 pl-4">
|
||||
<Show when={ServerConnection.key(current()) === key}>
|
||||
<p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
|
||||
<div class="flex flex-col h-full items-start w-5">
|
||||
<ServerHealthIndicator health={store.status[key]} />
|
||||
</div>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
dimmed={store.status[key]?.healthy === false}
|
||||
status={store.status[key]}
|
||||
class="flex items-center gap-3 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultUrl() === i.http.url}>
|
||||
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
showCredentials
|
||||
/>
|
||||
<div class="flex items-center justify-center gap-4 pl-4">
|
||||
<Show when={ServerConnection.key(current()) === key}>
|
||||
<Icon name="check" class="h-6" />
|
||||
</Show>
|
||||
|
||||
<Show when={i.type === "http"}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setStore("editServer", {
|
||||
id: i.http.url,
|
||||
value: i.http.url,
|
||||
error: "",
|
||||
status: store.status[ServerConnection.key(i)]?.healthy,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultUrl() !== i.http.url}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(i.http.url)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.default")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultUrl() === i.http.url}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<Show when={i.type === "http"}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
if (i.type !== "http") return
|
||||
startEdit(i)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultUrl() !== i.http.url}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(i.http.url)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.delete")}
|
||||
{language.t("dialog.server.menu.default")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultUrl() === i.http.url}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="px-5 pb-5">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus-small"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
setStore("addServer", { showForm: true, url: "", error: "" })
|
||||
scrollListToBottom()
|
||||
}}
|
||||
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
||||
<Show
|
||||
when={isFormMode()}
|
||||
fallback={
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus-small"
|
||||
size="large"
|
||||
onClick={startAdd}
|
||||
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
||||
>
|
||||
{language.t("dialog.server.add.button")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{store.addServer.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
|
||||
</Button>
|
||||
<Button variant="primary" size="large" onClick={submitForm} disabled={formBusy()} class="px-3 py-1.5">
|
||||
{formBusy()
|
||||
? language.t("dialog.server.add.checking")
|
||||
: isAddMode()
|
||||
? language.t("dialog.server.add.button")
|
||||
: language.t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { encodeFilePath } from "@/context/file/path"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
@@ -192,59 +191,6 @@ const FileTreeNode = (
|
||||
)
|
||||
}
|
||||
|
||||
const FileTreeNodeTooltip = (props: { enabled: boolean; node: FileNode; kind?: Kind; children: JSXElement }) => {
|
||||
if (!props.enabled) return props.children
|
||||
|
||||
const parts = props.node.path.split("/")
|
||||
const leaf = parts[parts.length - 1] ?? props.node.path
|
||||
const head = parts.slice(0, -1).join("/")
|
||||
const prefix = head ? `${head}/` : ""
|
||||
const label =
|
||||
props.kind === "add"
|
||||
? "Additions"
|
||||
: props.kind === "del"
|
||||
? "Deletions"
|
||||
: props.kind === "mix"
|
||||
? "Modifications"
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
openDelay={2000}
|
||||
placement="bottom-start"
|
||||
class="w-full"
|
||||
contentStyle={{ "max-width": "480px", width: "fit-content" }}
|
||||
value={
|
||||
<div class="flex items-center min-w-0 whitespace-nowrap text-12-regular">
|
||||
<span
|
||||
class="min-w-0 truncate text-text-invert-base"
|
||||
style={{ direction: "rtl", "unicode-bidi": "plaintext" }}
|
||||
>
|
||||
{prefix}
|
||||
</span>
|
||||
<span class="shrink-0 text-text-invert-strong">{leaf}</span>
|
||||
<Show when={label}>
|
||||
{(text) => (
|
||||
<>
|
||||
<span class="mx-1 font-bold text-text-invert-strong">•</span>
|
||||
<span class="shrink-0 text-text-invert-strong">{text()}</span>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={props.node.type === "directory" && props.node.ignored}>
|
||||
<>
|
||||
<span class="mx-1 font-bold text-text-invert-strong">•</span>
|
||||
<span class="shrink-0 text-text-invert-strong">Ignored</span>
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FileTree(props: {
|
||||
path: string
|
||||
class?: string
|
||||
@@ -255,7 +201,6 @@ export default function FileTree(props: {
|
||||
modified?: readonly string[]
|
||||
kinds?: ReadonlyMap<string, Kind>
|
||||
draggable?: boolean
|
||||
tooltip?: boolean
|
||||
onFileClick?: (file: FileNode) => void
|
||||
|
||||
_filter?: Filter
|
||||
@@ -267,7 +212,6 @@ export default function FileTree(props: {
|
||||
const file = useFile()
|
||||
const level = props.level ?? 0
|
||||
const draggable = () => props.draggable ?? true
|
||||
const tooltip = () => props.tooltip ?? true
|
||||
|
||||
const key = (p: string) =>
|
||||
file
|
||||
@@ -467,21 +411,19 @@ export default function FileTree(props: {
|
||||
onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
|
||||
>
|
||||
<Collapsible.Trigger>
|
||||
<FileTreeNodeTooltip enabled={tooltip()} node={node} kind={kind()}>
|
||||
<FileTreeNode
|
||||
node={node}
|
||||
level={level}
|
||||
active={props.active}
|
||||
nodeClass={props.nodeClass}
|
||||
draggable={draggable()}
|
||||
kinds={kinds()}
|
||||
marks={marks()}
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center text-icon-weak">
|
||||
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
|
||||
</div>
|
||||
</FileTreeNode>
|
||||
</FileTreeNodeTooltip>
|
||||
<FileTreeNode
|
||||
node={node}
|
||||
level={level}
|
||||
active={props.active}
|
||||
nodeClass={props.nodeClass}
|
||||
draggable={draggable()}
|
||||
kinds={kinds()}
|
||||
marks={marks()}
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center text-icon-weak">
|
||||
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
|
||||
</div>
|
||||
</FileTreeNode>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content class="relative pt-0.5">
|
||||
<div
|
||||
@@ -504,7 +446,6 @@ export default function FileTree(props: {
|
||||
kinds={props.kinds}
|
||||
active={props.active}
|
||||
draggable={props.draggable}
|
||||
tooltip={props.tooltip}
|
||||
onFileClick={props.onFileClick}
|
||||
_filter={filter()}
|
||||
_marks={marks()}
|
||||
@@ -517,53 +458,51 @@ export default function FileTree(props: {
|
||||
</Collapsible>
|
||||
</Match>
|
||||
<Match when={node.type === "file"}>
|
||||
<FileTreeNodeTooltip enabled={tooltip()} node={node} kind={kind()}>
|
||||
<FileTreeNode
|
||||
node={node}
|
||||
level={level}
|
||||
active={props.active}
|
||||
nodeClass={props.nodeClass}
|
||||
draggable={draggable()}
|
||||
kinds={kinds()}
|
||||
marks={marks()}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => props.onFileClick?.(node)}
|
||||
>
|
||||
<div class="w-4 shrink-0" />
|
||||
<Switch>
|
||||
<Match when={node.ignored}>
|
||||
<FileTreeNode
|
||||
node={node}
|
||||
level={level}
|
||||
active={props.active}
|
||||
nodeClass={props.nodeClass}
|
||||
draggable={draggable()}
|
||||
kinds={kinds()}
|
||||
marks={marks()}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => props.onFileClick?.(node)}
|
||||
>
|
||||
<div class="w-4 shrink-0" />
|
||||
<Switch>
|
||||
<Match when={node.ignored}>
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--mono"
|
||||
style="color: var(--icon-weak-base)"
|
||||
mono
|
||||
/>
|
||||
</Match>
|
||||
<Match when={active()}>
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--mono"
|
||||
style={kindTextColor(kind()!)}
|
||||
mono
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!node.ignored}>
|
||||
<span class="filetree-iconpair size-4">
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--mono"
|
||||
style="color: var(--icon-weak-base)"
|
||||
mono
|
||||
class="size-4 filetree-icon filetree-icon--color opacity-0 group-hover/filetree:opacity-100"
|
||||
/>
|
||||
</Match>
|
||||
<Match when={active()}>
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--mono"
|
||||
style={kindTextColor(kind()!)}
|
||||
class="size-4 filetree-icon filetree-icon--mono group-hover/filetree:opacity-0"
|
||||
mono
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!node.ignored}>
|
||||
<span class="filetree-iconpair size-4">
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--color opacity-0 group-hover/filetree:opacity-100"
|
||||
/>
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--mono group-hover/filetree:opacity-0"
|
||||
mono
|
||||
/>
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
</FileTreeNode>
|
||||
</FileTreeNodeTooltip>
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
</FileTreeNode>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { useFile } from "@/context/file"
|
||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||
import {
|
||||
ContentPart,
|
||||
DEFAULT_PROMPT,
|
||||
@@ -43,6 +43,9 @@ import {
|
||||
canNavigateHistoryAtCursor,
|
||||
navigatePromptHistory,
|
||||
prependHistoryEntry,
|
||||
type PromptHistoryComment,
|
||||
type PromptHistoryEntry,
|
||||
type PromptHistoryStoredEntry,
|
||||
promptLength,
|
||||
} from "./prompt-input/history"
|
||||
import { createPromptSubmit } from "./prompt-input/submit"
|
||||
@@ -170,12 +173,29 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const focus = { file: item.path, id: item.commentID }
|
||||
comments.setActive(focus)
|
||||
|
||||
const queueCommentFocus = (attempts = 6) => {
|
||||
const schedule = (left: number) => {
|
||||
requestAnimationFrame(() => {
|
||||
comments.setFocus({ ...focus })
|
||||
if (left <= 0) return
|
||||
requestAnimationFrame(() => {
|
||||
const current = comments.focus()
|
||||
if (!current) return
|
||||
if (current.file !== focus.file || current.id !== focus.id) return
|
||||
schedule(left - 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
schedule(attempts)
|
||||
}
|
||||
|
||||
const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
|
||||
if (wantsReview) {
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.setTab("changes")
|
||||
tabs().setActive("review")
|
||||
requestAnimationFrame(() => comments.setFocus(focus))
|
||||
queueCommentFocus()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -183,8 +203,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
layout.fileTree.setTab("all")
|
||||
const tab = files.tab(item.path)
|
||||
tabs().open(tab)
|
||||
files.load(item.path)
|
||||
requestAnimationFrame(() => comments.setFocus(focus))
|
||||
tabs().setActive(tab)
|
||||
Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
|
||||
}
|
||||
|
||||
const recent = createMemo(() => {
|
||||
@@ -219,7 +239,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const [store, setStore] = createStore<{
|
||||
popover: "at" | "slash" | null
|
||||
historyIndex: number
|
||||
savedPrompt: Prompt | null
|
||||
savedPrompt: PromptHistoryEntry | null
|
||||
placeholder: number
|
||||
draggingType: "image" | "@mention" | null
|
||||
mode: "normal" | "shell"
|
||||
@@ -227,7 +247,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}>({
|
||||
popover: null,
|
||||
historyIndex: -1,
|
||||
savedPrompt: null,
|
||||
savedPrompt: null as PromptHistoryEntry | null,
|
||||
placeholder: Math.floor(Math.random() * EXAMPLES.length),
|
||||
draggingType: null,
|
||||
mode: "normal",
|
||||
@@ -256,7 +276,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const [history, setHistory] = persisted(
|
||||
Persist.global("prompt-history", ["prompt-history.v1"]),
|
||||
createStore<{
|
||||
entries: Prompt[]
|
||||
entries: PromptHistoryStoredEntry[]
|
||||
}>({
|
||||
entries: [],
|
||||
}),
|
||||
@@ -264,7 +284,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const [shellHistory, setShellHistory] = persisted(
|
||||
Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]),
|
||||
createStore<{
|
||||
entries: Prompt[]
|
||||
entries: PromptHistoryStoredEntry[]
|
||||
}>({
|
||||
entries: [],
|
||||
}),
|
||||
@@ -282,9 +302,66 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}),
|
||||
)
|
||||
|
||||
const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
|
||||
const historyComments = () => {
|
||||
const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const))
|
||||
return prompt.context.items().flatMap((item) => {
|
||||
if (item.type !== "file") return []
|
||||
const comment = item.comment?.trim()
|
||||
if (!comment) return []
|
||||
|
||||
const selection = item.commentID ? byID.get(`${item.path}\n${item.commentID}`)?.selection : undefined
|
||||
const nextSelection =
|
||||
selection ??
|
||||
(item.selection
|
||||
? ({
|
||||
start: item.selection.startLine,
|
||||
end: item.selection.endLine,
|
||||
} satisfies SelectedLineRange)
|
||||
: undefined)
|
||||
if (!nextSelection) return []
|
||||
|
||||
return [
|
||||
{
|
||||
id: item.commentID ?? item.key,
|
||||
path: item.path,
|
||||
selection: { ...nextSelection },
|
||||
comment,
|
||||
time: item.commentID ? (byID.get(`${item.path}\n${item.commentID}`)?.time ?? Date.now()) : Date.now(),
|
||||
origin: item.commentOrigin,
|
||||
preview: item.preview,
|
||||
} satisfies PromptHistoryComment,
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const applyHistoryComments = (items: PromptHistoryComment[]) => {
|
||||
comments.replace(
|
||||
items.map((item) => ({
|
||||
id: item.id,
|
||||
file: item.path,
|
||||
selection: { ...item.selection },
|
||||
comment: item.comment,
|
||||
time: item.time,
|
||||
})),
|
||||
)
|
||||
prompt.context.replaceComments(
|
||||
items.map((item) => ({
|
||||
type: "file" as const,
|
||||
path: item.path,
|
||||
selection: selectionFromLines(item.selection),
|
||||
comment: item.comment,
|
||||
commentID: item.id,
|
||||
commentOrigin: item.origin,
|
||||
preview: item.preview,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
const applyHistoryPrompt = (entry: PromptHistoryEntry, position: "start" | "end") => {
|
||||
const p = entry.prompt
|
||||
const length = position === "start" ? 0 : promptLength(p)
|
||||
setStore("applyingHistory", true)
|
||||
applyHistoryComments(entry.comments)
|
||||
prompt.set(p, length)
|
||||
requestAnimationFrame(() => {
|
||||
editorRef.focus()
|
||||
@@ -846,7 +923,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
|
||||
const currentHistory = mode === "shell" ? shellHistory : history
|
||||
const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory
|
||||
const next = prependHistoryEntry(currentHistory.entries, prompt)
|
||||
const next = prependHistoryEntry(currentHistory.entries, prompt, mode === "shell" ? [] : historyComments())
|
||||
if (next === currentHistory.entries) return
|
||||
setCurrentHistory("entries", next)
|
||||
}
|
||||
@@ -857,12 +934,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
entries: store.mode === "shell" ? shellHistory.entries : history.entries,
|
||||
historyIndex: store.historyIndex,
|
||||
currentPrompt: prompt.current(),
|
||||
currentComments: historyComments(),
|
||||
savedPrompt: store.savedPrompt,
|
||||
})
|
||||
if (!result.handled) return false
|
||||
setStore("historyIndex", result.historyIndex)
|
||||
setStore("savedPrompt", result.savedPrompt)
|
||||
applyHistoryPrompt(result.prompt, result.cursor)
|
||||
applyHistoryPrompt(result.entry, result.cursor)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1048,6 +1126,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
|
||||
const variants = createMemo(() => ["default", ...local.model.variant.list()])
|
||||
const accepting = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return false
|
||||
return permission.isAutoAccepting(id, sdk.directory)
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||
@@ -1233,7 +1316,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.permissions.autoaccept.enable")}
|
||||
title={language.t(
|
||||
accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable",
|
||||
)}
|
||||
keybind={command.keybind("permissions.autoaccept")}
|
||||
>
|
||||
<Button
|
||||
@@ -1242,20 +1327,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
|
||||
classList={{
|
||||
"_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
|
||||
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
|
||||
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
||||
"text-text-base": !accepting(),
|
||||
"hover:bg-surface-success-base": accepting(),
|
||||
}}
|
||||
aria-label={
|
||||
permission.isAutoAccepting(params.id!, sdk.directory)
|
||||
accepting()
|
||||
? language.t("command.permissions.autoaccept.disable")
|
||||
: language.t("command.permissions.autoaccept.enable")
|
||||
}
|
||||
aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)}
|
||||
aria-pressed={accepting()}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-double-right"
|
||||
size="small"
|
||||
classList={{ "text-icon-success-base": permission.isAutoAccepting(params.id!, sdk.directory) }}
|
||||
classList={{ "text-icon-success-base": accepting() }}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
|
||||
@@ -35,6 +35,15 @@ describe("buildRequestParts", () => {
|
||||
result.requestParts.some((part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts")),
|
||||
).toBe(true)
|
||||
expect(result.requestParts.some((part) => part.type === "text" && part.synthetic)).toBe(true)
|
||||
expect(
|
||||
result.requestParts.some(
|
||||
(part) =>
|
||||
part.type === "text" &&
|
||||
part.synthetic &&
|
||||
part.metadata?.opencodeComment &&
|
||||
(part.metadata.opencodeComment as { comment?: string }).comment === "check this",
|
||||
),
|
||||
).toBe(true)
|
||||
|
||||
expect(result.optimisticParts).toHaveLength(result.requestParts.length)
|
||||
expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true)
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { FileSelection } from "@/context/file"
|
||||
import { encodeFilePath } from "@/context/file/path"
|
||||
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { createCommentMetadata, formatCommentNote } from "@/utils/comment-note"
|
||||
|
||||
type PromptRequestPart = (TextPartInput | FilePartInput | AgentPartInput) & { id: string }
|
||||
|
||||
@@ -41,18 +42,6 @@ const fileQuery = (selection: FileSelection | undefined) =>
|
||||
const isFileAttachment = (part: Prompt[number]): part is FileAttachmentPart => part.type === "file"
|
||||
const isAgentAttachment = (part: Prompt[number]): part is AgentPart => part.type === "agent"
|
||||
|
||||
const commentNote = (path: string, selection: FileSelection | undefined, comment: string) => {
|
||||
const start = selection ? Math.min(selection.startLine, selection.endLine) : undefined
|
||||
const end = selection ? Math.max(selection.startLine, selection.endLine) : undefined
|
||||
const range =
|
||||
start === undefined || end === undefined
|
||||
? "this file"
|
||||
: start === end
|
||||
? `line ${start}`
|
||||
: `lines ${start} through ${end}`
|
||||
return `The user made the following comment regarding ${range} of ${path}: ${comment}`
|
||||
}
|
||||
|
||||
const toOptimisticPart = (part: PromptRequestPart, sessionID: string, messageID: string): Part => {
|
||||
if (part.type === "text") {
|
||||
return {
|
||||
@@ -153,8 +142,15 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
type: "text",
|
||||
text: commentNote(item.path, item.selection, comment),
|
||||
text: formatCommentNote({ path: item.path, selection: item.selection, comment }),
|
||||
synthetic: true,
|
||||
metadata: createCommentMetadata({
|
||||
path: item.path,
|
||||
selection: item.selection,
|
||||
comment,
|
||||
preview: item.preview,
|
||||
origin: item.commentOrigin,
|
||||
}),
|
||||
} satisfies PromptRequestPart,
|
||||
filePart,
|
||||
]
|
||||
|
||||
@@ -3,25 +3,42 @@ import type { Prompt } from "@/context/prompt"
|
||||
import {
|
||||
canNavigateHistoryAtCursor,
|
||||
clonePromptParts,
|
||||
normalizePromptHistoryEntry,
|
||||
navigatePromptHistory,
|
||||
prependHistoryEntry,
|
||||
promptLength,
|
||||
type PromptHistoryComment,
|
||||
} from "./history"
|
||||
|
||||
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
const text = (value: string): Prompt => [{ type: "text", content: value, start: 0, end: value.length }]
|
||||
const comment = (id: string, value = "note"): PromptHistoryComment => ({
|
||||
id,
|
||||
path: "src/a.ts",
|
||||
selection: { start: 2, end: 4 },
|
||||
comment: value,
|
||||
time: 1,
|
||||
origin: "review",
|
||||
preview: "const a = 1",
|
||||
})
|
||||
|
||||
describe("prompt-input history", () => {
|
||||
test("prependHistoryEntry skips empty prompt and deduplicates consecutive entries", () => {
|
||||
const first = prependHistoryEntry([], DEFAULT_PROMPT)
|
||||
expect(first).toEqual([])
|
||||
|
||||
const commentsOnly = prependHistoryEntry([], DEFAULT_PROMPT, [comment("c1")])
|
||||
expect(commentsOnly).toHaveLength(1)
|
||||
|
||||
const withOne = prependHistoryEntry([], text("hello"))
|
||||
expect(withOne).toHaveLength(1)
|
||||
|
||||
const deduped = prependHistoryEntry(withOne, text("hello"))
|
||||
expect(deduped).toBe(withOne)
|
||||
|
||||
const dedupedComments = prependHistoryEntry(commentsOnly, DEFAULT_PROMPT, [comment("c1")])
|
||||
expect(dedupedComments).toBe(commentsOnly)
|
||||
})
|
||||
|
||||
test("navigatePromptHistory restores saved prompt when moving down from newest", () => {
|
||||
@@ -31,24 +48,57 @@ describe("prompt-input history", () => {
|
||||
entries,
|
||||
historyIndex: -1,
|
||||
currentPrompt: text("draft"),
|
||||
currentComments: [comment("draft")],
|
||||
savedPrompt: null,
|
||||
})
|
||||
expect(up.handled).toBe(true)
|
||||
if (!up.handled) throw new Error("expected handled")
|
||||
expect(up.historyIndex).toBe(0)
|
||||
expect(up.cursor).toBe("start")
|
||||
expect(up.entry.comments).toEqual([])
|
||||
|
||||
const down = navigatePromptHistory({
|
||||
direction: "down",
|
||||
entries,
|
||||
historyIndex: up.historyIndex,
|
||||
currentPrompt: text("ignored"),
|
||||
currentComments: [],
|
||||
savedPrompt: up.savedPrompt,
|
||||
})
|
||||
expect(down.handled).toBe(true)
|
||||
if (!down.handled) throw new Error("expected handled")
|
||||
expect(down.historyIndex).toBe(-1)
|
||||
expect(down.prompt[0]?.type === "text" ? down.prompt[0].content : "").toBe("draft")
|
||||
expect(down.entry.prompt[0]?.type === "text" ? down.entry.prompt[0].content : "").toBe("draft")
|
||||
expect(down.entry.comments).toEqual([comment("draft")])
|
||||
})
|
||||
|
||||
test("navigatePromptHistory keeps entry comments when moving through history", () => {
|
||||
const entries = [
|
||||
{
|
||||
prompt: text("with comment"),
|
||||
comments: [comment("c1")],
|
||||
},
|
||||
]
|
||||
|
||||
const up = navigatePromptHistory({
|
||||
direction: "up",
|
||||
entries,
|
||||
historyIndex: -1,
|
||||
currentPrompt: text("draft"),
|
||||
currentComments: [],
|
||||
savedPrompt: null,
|
||||
})
|
||||
|
||||
expect(up.handled).toBe(true)
|
||||
if (!up.handled) throw new Error("expected handled")
|
||||
expect(up.entry.prompt[0]?.type === "text" ? up.entry.prompt[0].content : "").toBe("with comment")
|
||||
expect(up.entry.comments).toEqual([comment("c1")])
|
||||
})
|
||||
|
||||
test("normalizePromptHistoryEntry supports legacy prompt arrays", () => {
|
||||
const entry = normalizePromptHistoryEntry(text("legacy"))
|
||||
expect(entry.prompt[0]?.type === "text" ? entry.prompt[0].content : "").toBe("legacy")
|
||||
expect(entry.comments).toEqual([])
|
||||
})
|
||||
|
||||
test("helpers clone prompt and count text content length", () => {
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import type { Prompt } from "@/context/prompt"
|
||||
import type { SelectedLineRange } from "@/context/file"
|
||||
|
||||
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
export const MAX_HISTORY = 100
|
||||
|
||||
export type PromptHistoryComment = {
|
||||
id: string
|
||||
path: string
|
||||
selection: SelectedLineRange
|
||||
comment: string
|
||||
time: number
|
||||
origin?: "review" | "file"
|
||||
preview?: string
|
||||
}
|
||||
|
||||
export type PromptHistoryEntry = {
|
||||
prompt: Prompt
|
||||
comments: PromptHistoryComment[]
|
||||
}
|
||||
|
||||
export type PromptHistoryStoredEntry = Prompt | PromptHistoryEntry
|
||||
|
||||
export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) {
|
||||
const position = Math.max(0, Math.min(cursor, text.length))
|
||||
const atStart = position === 0
|
||||
@@ -25,29 +43,82 @@ export function clonePromptParts(prompt: Prompt): Prompt {
|
||||
})
|
||||
}
|
||||
|
||||
function cloneSelection(selection: SelectedLineRange): SelectedLineRange {
|
||||
return {
|
||||
start: selection.start,
|
||||
end: selection.end,
|
||||
...(selection.side ? { side: selection.side } : {}),
|
||||
...(selection.endSide ? { endSide: selection.endSide } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function clonePromptHistoryComments(comments: PromptHistoryComment[]) {
|
||||
return comments.map((comment) => ({
|
||||
...comment,
|
||||
selection: cloneSelection(comment.selection),
|
||||
}))
|
||||
}
|
||||
|
||||
export function normalizePromptHistoryEntry(entry: PromptHistoryStoredEntry): PromptHistoryEntry {
|
||||
if (Array.isArray(entry)) {
|
||||
return {
|
||||
prompt: clonePromptParts(entry),
|
||||
comments: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
prompt: clonePromptParts(entry.prompt),
|
||||
comments: clonePromptHistoryComments(entry.comments),
|
||||
}
|
||||
}
|
||||
|
||||
export function promptLength(prompt: Prompt) {
|
||||
return prompt.reduce((len, part) => len + ("content" in part ? part.content.length : 0), 0)
|
||||
}
|
||||
|
||||
export function prependHistoryEntry(entries: Prompt[], prompt: Prompt, max = MAX_HISTORY) {
|
||||
export function prependHistoryEntry(
|
||||
entries: PromptHistoryStoredEntry[],
|
||||
prompt: Prompt,
|
||||
comments: PromptHistoryComment[] = [],
|
||||
max = MAX_HISTORY,
|
||||
) {
|
||||
const text = prompt
|
||||
.map((part) => ("content" in part ? part.content : ""))
|
||||
.join("")
|
||||
.trim()
|
||||
const hasImages = prompt.some((part) => part.type === "image")
|
||||
if (!text && !hasImages) return entries
|
||||
const hasComments = comments.some((comment) => !!comment.comment.trim())
|
||||
if (!text && !hasImages && !hasComments) return entries
|
||||
|
||||
const entry = clonePromptParts(prompt)
|
||||
const entry = {
|
||||
prompt: clonePromptParts(prompt),
|
||||
comments: clonePromptHistoryComments(comments),
|
||||
} satisfies PromptHistoryEntry
|
||||
const last = entries[0]
|
||||
if (last && isPromptEqual(last, entry)) return entries
|
||||
return [entry, ...entries].slice(0, max)
|
||||
}
|
||||
|
||||
function isPromptEqual(promptA: Prompt, promptB: Prompt) {
|
||||
if (promptA.length !== promptB.length) return false
|
||||
for (let i = 0; i < promptA.length; i++) {
|
||||
const partA = promptA[i]
|
||||
const partB = promptB[i]
|
||||
function isCommentEqual(commentA: PromptHistoryComment, commentB: PromptHistoryComment) {
|
||||
return (
|
||||
commentA.path === commentB.path &&
|
||||
commentA.comment === commentB.comment &&
|
||||
commentA.origin === commentB.origin &&
|
||||
commentA.preview === commentB.preview &&
|
||||
commentA.selection.start === commentB.selection.start &&
|
||||
commentA.selection.end === commentB.selection.end &&
|
||||
commentA.selection.side === commentB.selection.side &&
|
||||
commentA.selection.endSide === commentB.selection.endSide
|
||||
)
|
||||
}
|
||||
|
||||
function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistoryStoredEntry) {
|
||||
const entryA = normalizePromptHistoryEntry(promptA)
|
||||
const entryB = normalizePromptHistoryEntry(promptB)
|
||||
if (entryA.prompt.length !== entryB.prompt.length) return false
|
||||
for (let i = 0; i < entryA.prompt.length; i++) {
|
||||
const partA = entryA.prompt[i]
|
||||
const partB = entryB.prompt[i]
|
||||
if (partA.type !== partB.type) return false
|
||||
if (partA.type === "text" && partA.content !== (partB.type === "text" ? partB.content : "")) return false
|
||||
if (partA.type === "file") {
|
||||
@@ -67,28 +138,35 @@ function isPromptEqual(promptA: Prompt, promptB: Prompt) {
|
||||
if (partA.type === "agent" && partA.name !== (partB.type === "agent" ? partB.name : "")) return false
|
||||
if (partA.type === "image" && partA.id !== (partB.type === "image" ? partB.id : "")) return false
|
||||
}
|
||||
if (entryA.comments.length !== entryB.comments.length) return false
|
||||
for (let i = 0; i < entryA.comments.length; i++) {
|
||||
const commentA = entryA.comments[i]
|
||||
const commentB = entryB.comments[i]
|
||||
if (!commentA || !commentB || !isCommentEqual(commentA, commentB)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type HistoryNavInput = {
|
||||
direction: "up" | "down"
|
||||
entries: Prompt[]
|
||||
entries: PromptHistoryStoredEntry[]
|
||||
historyIndex: number
|
||||
currentPrompt: Prompt
|
||||
savedPrompt: Prompt | null
|
||||
currentComments: PromptHistoryComment[]
|
||||
savedPrompt: PromptHistoryEntry | null
|
||||
}
|
||||
|
||||
type HistoryNavResult =
|
||||
| {
|
||||
handled: false
|
||||
historyIndex: number
|
||||
savedPrompt: Prompt | null
|
||||
savedPrompt: PromptHistoryEntry | null
|
||||
}
|
||||
| {
|
||||
handled: true
|
||||
historyIndex: number
|
||||
savedPrompt: Prompt | null
|
||||
prompt: Prompt
|
||||
savedPrompt: PromptHistoryEntry | null
|
||||
entry: PromptHistoryEntry
|
||||
cursor: "start" | "end"
|
||||
}
|
||||
|
||||
@@ -103,22 +181,27 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
||||
}
|
||||
|
||||
if (input.historyIndex === -1) {
|
||||
const entry = normalizePromptHistoryEntry(input.entries[0])
|
||||
return {
|
||||
handled: true,
|
||||
historyIndex: 0,
|
||||
savedPrompt: clonePromptParts(input.currentPrompt),
|
||||
prompt: input.entries[0],
|
||||
savedPrompt: {
|
||||
prompt: clonePromptParts(input.currentPrompt),
|
||||
comments: clonePromptHistoryComments(input.currentComments),
|
||||
},
|
||||
entry,
|
||||
cursor: "start",
|
||||
}
|
||||
}
|
||||
|
||||
if (input.historyIndex < input.entries.length - 1) {
|
||||
const next = input.historyIndex + 1
|
||||
const entry = normalizePromptHistoryEntry(input.entries[next])
|
||||
return {
|
||||
handled: true,
|
||||
historyIndex: next,
|
||||
savedPrompt: input.savedPrompt,
|
||||
prompt: input.entries[next],
|
||||
entry,
|
||||
cursor: "start",
|
||||
}
|
||||
}
|
||||
@@ -132,11 +215,12 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
||||
|
||||
if (input.historyIndex > 0) {
|
||||
const next = input.historyIndex - 1
|
||||
const entry = normalizePromptHistoryEntry(input.entries[next])
|
||||
return {
|
||||
handled: true,
|
||||
historyIndex: next,
|
||||
savedPrompt: input.savedPrompt,
|
||||
prompt: input.entries[next],
|
||||
entry,
|
||||
cursor: "end",
|
||||
}
|
||||
}
|
||||
@@ -147,7 +231,7 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
||||
handled: true,
|
||||
historyIndex: -1,
|
||||
savedPrompt: null,
|
||||
prompt: input.savedPrompt,
|
||||
entry: input.savedPrompt,
|
||||
cursor: "end",
|
||||
}
|
||||
}
|
||||
@@ -156,7 +240,10 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
||||
handled: true,
|
||||
historyIndex: -1,
|
||||
savedPrompt: null,
|
||||
prompt: DEFAULT_PROMPT,
|
||||
entry: {
|
||||
prompt: DEFAULT_PROMPT,
|
||||
comments: [],
|
||||
},
|
||||
cursor: "end",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import {
|
||||
children,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
type ParentProps,
|
||||
Show,
|
||||
} from "solid-js"
|
||||
import { type ServerConnection, serverDisplayName } from "@/context/server"
|
||||
import { type ServerConnection, serverName } from "@/context/server"
|
||||
import type { ServerHealth } from "@/utils/server-health"
|
||||
|
||||
interface ServerRowProps extends ParentProps {
|
||||
@@ -20,13 +21,14 @@ interface ServerRowProps extends ParentProps {
|
||||
versionClass?: string
|
||||
dimmed?: boolean
|
||||
badge?: JSXElement
|
||||
showCredentials?: boolean
|
||||
}
|
||||
|
||||
export function ServerRow(props: ServerRowProps) {
|
||||
const [truncated, setTruncated] = createSignal(false)
|
||||
let nameRef: HTMLSpanElement | undefined
|
||||
let versionRef: HTMLSpanElement | undefined
|
||||
const name = createMemo(() => serverDisplayName(props.conn))
|
||||
const name = createMemo(() => serverName(props.conn))
|
||||
|
||||
const check = () => {
|
||||
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
|
||||
@@ -52,35 +54,71 @@ export function ServerRow(props: ServerRowProps) {
|
||||
|
||||
const tooltipValue = () => (
|
||||
<span class="flex items-center gap-2">
|
||||
<span>{name()}</span>
|
||||
<span>{serverName(props.conn, true)}</span>
|
||||
<Show when={props.status?.version}>
|
||||
<span class="text-text-invert-base">{props.status?.version}</span>
|
||||
<span class="text-text-invert-weak">v{props.status?.version}</span>
|
||||
</Show>
|
||||
</span>
|
||||
)
|
||||
|
||||
const badge = children(() => props.badge)
|
||||
|
||||
return (
|
||||
<Tooltip value={tooltipValue()} placement="top" inactive={!truncated()}>
|
||||
<Tooltip
|
||||
class="flex-1"
|
||||
value={tooltipValue()}
|
||||
placement="top-start"
|
||||
inactive={!truncated() && !props.conn.displayName}
|
||||
>
|
||||
<div class={props.class} classList={{ "opacity-50": props.dimmed }}>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full shrink-0": true,
|
||||
"bg-icon-success-base": props.status?.healthy === true,
|
||||
"bg-icon-critical-base": props.status?.healthy === false,
|
||||
"bg-border-weak-base": props.status === undefined,
|
||||
}}
|
||||
/>
|
||||
<span ref={nameRef} class={props.nameClass ?? "truncate"}>
|
||||
{name()}
|
||||
</span>
|
||||
<Show when={props.status?.version}>
|
||||
<span ref={versionRef} class={props.versionClass ?? "text-text-weak text-14-regular truncate"}>
|
||||
{props.status?.version}
|
||||
</span>
|
||||
</Show>
|
||||
{props.badge}
|
||||
<div class="flex flex-col items-start">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<span ref={nameRef} class={props.nameClass ?? "truncate"}>
|
||||
{name()}
|
||||
</span>
|
||||
<Show
|
||||
when={badge()}
|
||||
fallback={
|
||||
<Show when={props.status?.version}>
|
||||
<span ref={versionRef} class={props.versionClass ?? "text-text-weak text-14-regular truncate"}>
|
||||
v{props.status?.version}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
{(badge) => badge()}
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={props.showCredentials && props.conn.type === "http" && props.conn}>
|
||||
{(conn) => (
|
||||
<div class="flex flex-row gap-3">
|
||||
<span>
|
||||
{conn().http.username ? (
|
||||
<span class="text-text-weak">{conn().http.username}</span>
|
||||
) : (
|
||||
<span class="text-text-weaker">no username</span>
|
||||
)}
|
||||
</span>
|
||||
{conn().http.password && <span class="text-text-weak">••••••••</span>}
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServerHealthIndicator(props: { health?: ServerHealth }) {
|
||||
return (
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full shrink-0": true,
|
||||
"bg-icon-success-base": props.health?.healthy === true,
|
||||
"bg-icon-critical-base": props.health?.healthy === false,
|
||||
"bg-border-weak-base": props.health === undefined,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { same } from "@/utils/same"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { File } from "@opencode-ai/ui/file"
|
||||
import { Markdown } from "@opencode-ai/ui/markdown"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
@@ -47,7 +47,8 @@ function RawMessageContent(props: { message: Message; getParts: (id: string) =>
|
||||
})
|
||||
|
||||
return (
|
||||
<Code
|
||||
<File
|
||||
mode="text"
|
||||
file={file()}
|
||||
overflow="wrap"
|
||||
class="select-text"
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { AppIcon } from "@opencode-ai/ui/app-icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { AppIcon } from "@opencode-ai/ui/app-icon"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { StatusPopover } from "../status-popover"
|
||||
|
||||
const OPEN_APPS = [
|
||||
@@ -45,32 +45,67 @@ type OpenApp = (typeof OPEN_APPS)[number]
|
||||
type OS = "macos" | "windows" | "linux" | "unknown"
|
||||
|
||||
const MAC_APPS = [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "Visual Studio Code" },
|
||||
{
|
||||
id: "vscode",
|
||||
label: "VS Code",
|
||||
icon: "vscode",
|
||||
openWith: "Visual Studio Code",
|
||||
},
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
|
||||
{ id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
|
||||
{ id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity" },
|
||||
{
|
||||
id: "antigravity",
|
||||
label: "Antigravity",
|
||||
icon: "antigravity",
|
||||
openWith: "Antigravity",
|
||||
},
|
||||
{ id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
|
||||
{ id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
|
||||
{ id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
|
||||
{ id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
|
||||
{ id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio" },
|
||||
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
|
||||
{
|
||||
id: "android-studio",
|
||||
label: "Android Studio",
|
||||
icon: "android-studio",
|
||||
openWith: "Android Studio",
|
||||
},
|
||||
{
|
||||
id: "sublime-text",
|
||||
label: "Sublime Text",
|
||||
icon: "sublime-text",
|
||||
openWith: "Sublime Text",
|
||||
},
|
||||
] as const
|
||||
|
||||
const WINDOWS_APPS = [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
||||
{ id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
|
||||
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
|
||||
{
|
||||
id: "powershell",
|
||||
label: "PowerShell",
|
||||
icon: "powershell",
|
||||
openWith: "powershell",
|
||||
},
|
||||
{
|
||||
id: "sublime-text",
|
||||
label: "Sublime Text",
|
||||
icon: "sublime-text",
|
||||
openWith: "Sublime Text",
|
||||
},
|
||||
] as const
|
||||
|
||||
const LINUX_APPS = [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
||||
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
|
||||
{
|
||||
id: "sublime-text",
|
||||
label: "Sublime Text",
|
||||
icon: "sublime-text",
|
||||
openWith: "Sublime Text",
|
||||
},
|
||||
] as const
|
||||
|
||||
type OpenOption = (typeof MAC_APPS)[number] | (typeof WINDOWS_APPS)[number] | (typeof LINUX_APPS)[number]
|
||||
@@ -213,7 +248,9 @@ export function SessionHeader() {
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const os = createMemo(() => detectOS(platform))
|
||||
|
||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ finder: true })
|
||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||
finder: true,
|
||||
})
|
||||
|
||||
const apps = createMemo(() => {
|
||||
if (os() === "macos") return MAC_APPS
|
||||
@@ -259,18 +296,34 @@ export function SessionHeader() {
|
||||
|
||||
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
||||
const [menu, setMenu] = createStore({ open: false })
|
||||
const [openRequest, setOpenRequest] = createStore({
|
||||
app: undefined as OpenApp | undefined,
|
||||
})
|
||||
|
||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
||||
const opening = createMemo(() => openRequest.app !== undefined)
|
||||
|
||||
createEffect(() => {
|
||||
const value = prefs.app
|
||||
if (options().some((o) => o.id === value)) return
|
||||
setPrefs("app", options()[0]?.id ?? "finder")
|
||||
})
|
||||
|
||||
const openDir = (app: OpenApp) => {
|
||||
if (opening() || !canOpen() || !platform.openPath) return
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return
|
||||
if (!canOpen()) return
|
||||
|
||||
const item = options().find((o) => o.id === app)
|
||||
const openWith = item && "openWith" in item ? item.openWith : undefined
|
||||
Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => showRequestError(language, err))
|
||||
setOpenRequest("app", app)
|
||||
platform
|
||||
.openPath(directory, openWith)
|
||||
.catch((err: unknown) => showRequestError(language, err))
|
||||
.finally(() => {
|
||||
setOpenRequest("app", undefined)
|
||||
})
|
||||
}
|
||||
|
||||
const copyPath = () => {
|
||||
@@ -315,7 +368,9 @@ export function SessionHeader() {
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1.5 overflow-visible">
|
||||
<Icon name="magnifying-glass" size="small" class="icon-base shrink-0 size-4" />
|
||||
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
|
||||
{language.t("session.header.search.placeholder", { project: name() })}
|
||||
{language.t("session.header.search.placeholder", {
|
||||
project: name(),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -357,12 +412,21 @@ export function SessionHeader() {
|
||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
|
||||
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none disabled:!cursor-default"
|
||||
classList={{
|
||||
"bg-surface-raised-base-active": opening(),
|
||||
}}
|
||||
onClick={() => openDir(current().id)}
|
||||
disabled={opening()}
|
||||
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
||||
>
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<AppIcon id={current().icon} class="size-4" />
|
||||
<Show
|
||||
when={opening()}
|
||||
fallback={<AppIcon id={current().icon} class={openIconSize(current().icon)} />}
|
||||
>
|
||||
<Spinner class="size-3.5 text-icon-base" />
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-12-regular text-text-strong">Open</span>
|
||||
</Button>
|
||||
@@ -377,7 +441,11 @@ export function SessionHeader() {
|
||||
as={IconButton}
|
||||
icon="chevron-down"
|
||||
variant="ghost"
|
||||
class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-hover"
|
||||
disabled={opening()}
|
||||
class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
|
||||
classList={{
|
||||
"bg-surface-raised-base-active": opening(),
|
||||
}}
|
||||
aria-label={language.t("session.header.open.menu")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
@@ -395,6 +463,7 @@ export function SessionHeader() {
|
||||
{(o) => (
|
||||
<DropdownMenu.RadioItem
|
||||
value={o.id}
|
||||
disabled={opening()}
|
||||
onSelect={() => {
|
||||
setMenu("open", false)
|
||||
openDir(o.id)
|
||||
|
||||
@@ -13,13 +13,15 @@ import { useCommand } from "@/context/command"
|
||||
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
||||
return (
|
||||
<div class="flex items-center gap-x-1.5 min-w-0">
|
||||
<FileIcon
|
||||
node={{ path: props.path, type: "file" }}
|
||||
classList={{
|
||||
"grayscale-100 group-data-[selected]/tab:grayscale-0": !props.active,
|
||||
"grayscale-0": props.active,
|
||||
}}
|
||||
/>
|
||||
<Show
|
||||
when={!props.active}
|
||||
fallback={<FileIcon node={{ path: props.path, type: "file" }} class="size-4 shrink-0" />}
|
||||
>
|
||||
<span class="relative inline-flex size-4 shrink-0">
|
||||
<FileIcon node={{ path: props.path, type: "file" }} class="absolute inset-0 size-4 tab-fileicon-color" />
|
||||
<FileIcon node={{ path: props.path, type: "file" }} mono class="absolute inset-0 size-4 tab-fileicon-mono" />
|
||||
</span>
|
||||
</Show>
|
||||
<span class="text-14-medium truncate">{getFilename(props.path)}</span>
|
||||
</div>
|
||||
)
|
||||
@@ -37,8 +39,8 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
||||
return <FileVisual path={value} />
|
||||
})
|
||||
return (
|
||||
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
|
||||
<div class="relative h-full">
|
||||
<div use:sortable class="h-full flex items-center" classList={{ "opacity-0": sortable.isActiveDraggable }}>
|
||||
<div class="relative">
|
||||
<Tabs.Trigger
|
||||
value={props.tab}
|
||||
closeButton={
|
||||
@@ -46,6 +48,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
||||
title={language.t("common.closeTab")}
|
||||
keybind={command.keybind("tab.close")}
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
|
||||
@@ -187,9 +187,22 @@ export const SettingsProviders: Component = () => {
|
||||
<div class="flex items-center gap-x-3">
|
||||
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{item.name}</span>
|
||||
<Show when={item.id === "opencode"}>
|
||||
<span class="text-14-regular text-text-weak">
|
||||
{language.t("dialog.provider.opencode.tagline")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item.id === "opencode"}>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
<Show when={item.id === "opencode-go"}>
|
||||
<>
|
||||
<span class="text-14-regular text-text-weak">
|
||||
{language.t("dialog.provider.opencodeGo.tagline")}
|
||||
</span>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={note(item.id)}>
|
||||
{(key) => <span class="text-12-regular text-text-weak pl-8">{language.t(key())}</span>}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { type Accessor, createEffect, createMemo, createSignal, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { ServerRow } from "@/components/server/server-row"
|
||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
@@ -276,10 +276,11 @@ export function StatusPopover() {
|
||||
navigate("/")
|
||||
}}
|
||||
>
|
||||
<ServerHealthIndicator health={health[key]} />
|
||||
<ServerRow
|
||||
conn={s}
|
||||
status={health[key]}
|
||||
dimmed={isBlocked()}
|
||||
status={health[key]}
|
||||
class="flex items-center gap-2 w-full min-w-0"
|
||||
nameClass="text-14-regular text-text-base truncate"
|
||||
versionClass="text-12-regular text-text-weak truncate"
|
||||
|
||||
@@ -265,6 +265,9 @@ export function Titlebar() {
|
||||
</div>
|
||||
</div>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
<div class="bg-icon-interactive-base text-background-base font-medium px-2 rounded-sm uppercase font-mono">
|
||||
BETA
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||
|
||||
@@ -150,4 +150,37 @@ describe("comments session indexing", () => {
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("update changes only the targeted comment body", () => {
|
||||
createRoot((dispose) => {
|
||||
const comments = createCommentSessionForTest({
|
||||
"a.ts": [line("a.ts", "a1", 10), line("a.ts", "a2", 20)],
|
||||
})
|
||||
|
||||
comments.update("a.ts", "a2", "edited")
|
||||
|
||||
expect(comments.list("a.ts").map((item) => item.comment)).toEqual(["a1", "edited"])
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("replace swaps comment state and clears focus state", () => {
|
||||
createRoot((dispose) => {
|
||||
const comments = createCommentSessionForTest({
|
||||
"a.ts": [line("a.ts", "a1", 10)],
|
||||
})
|
||||
|
||||
comments.setFocus({ file: "a.ts", id: "a1" })
|
||||
comments.setActive({ file: "a.ts", id: "a1" })
|
||||
comments.replace([line("b.ts", "b1", 30)])
|
||||
|
||||
expect(comments.list("a.ts")).toEqual([])
|
||||
expect(comments.list("b.ts").map((item) => item.id)).toEqual(["b1"])
|
||||
expect(comments.focus()).toBeNull()
|
||||
expect(comments.active()).toBeNull()
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -44,6 +44,37 @@ function aggregate(comments: Record<string, LineComment[]>) {
|
||||
.sort((a, b) => a.time - b.time)
|
||||
}
|
||||
|
||||
function cloneSelection(selection: SelectedLineRange): SelectedLineRange {
|
||||
const next: SelectedLineRange = {
|
||||
start: selection.start,
|
||||
end: selection.end,
|
||||
}
|
||||
|
||||
if (selection.side) next.side = selection.side
|
||||
if (selection.endSide) next.endSide = selection.endSide
|
||||
return next
|
||||
}
|
||||
|
||||
function cloneComment(comment: LineComment): LineComment {
|
||||
return {
|
||||
...comment,
|
||||
selection: cloneSelection(comment.selection),
|
||||
}
|
||||
}
|
||||
|
||||
function group(comments: LineComment[]) {
|
||||
return comments.reduce<Record<string, LineComment[]>>((acc, comment) => {
|
||||
const list = acc[comment.file]
|
||||
const next = cloneComment(comment)
|
||||
if (list) {
|
||||
list.push(next)
|
||||
return acc
|
||||
}
|
||||
acc[comment.file] = [next]
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
function createCommentSessionState(store: Store<CommentStore>, setStore: SetStoreFunction<CommentStore>) {
|
||||
const [state, setState] = createStore({
|
||||
focus: null as CommentFocus | null,
|
||||
@@ -70,6 +101,7 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
|
||||
id: uuid(),
|
||||
time: Date.now(),
|
||||
...input,
|
||||
selection: cloneSelection(input.selection),
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
@@ -87,6 +119,23 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
|
||||
})
|
||||
}
|
||||
|
||||
const update = (file: string, id: string, comment: string) => {
|
||||
setStore("comments", file, (items) =>
|
||||
(items ?? []).map((item) => {
|
||||
if (item.id !== id) return item
|
||||
return { ...item, comment }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const replace = (comments: LineComment[]) => {
|
||||
batch(() => {
|
||||
setStore("comments", reconcile(group(comments)))
|
||||
setFocus(null)
|
||||
setActive(null)
|
||||
})
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
batch(() => {
|
||||
setStore("comments", reconcile({}))
|
||||
@@ -100,6 +149,8 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
|
||||
all,
|
||||
add,
|
||||
remove,
|
||||
update,
|
||||
replace,
|
||||
clear,
|
||||
focus: () => state.focus,
|
||||
setFocus,
|
||||
@@ -132,6 +183,8 @@ function createCommentSession(dir: string, id: string | undefined) {
|
||||
all: session.all,
|
||||
add: session.add,
|
||||
remove: session.remove,
|
||||
update: session.update,
|
||||
replace: session.replace,
|
||||
clear: session.clear,
|
||||
focus: session.focus,
|
||||
setFocus: session.setFocus,
|
||||
@@ -176,6 +229,8 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
|
||||
all: () => session().all(),
|
||||
add: (input: Omit<LineComment, "id" | "time">) => session().add(input),
|
||||
remove: (file: string, id: string) => session().remove(file, id),
|
||||
update: (file: string, id: string, comment: string) => session().update(file, id, comment),
|
||||
replace: (comments: LineComment[]) => session().replace(comments),
|
||||
clear: () => session().clear(),
|
||||
focus: () => session().focus(),
|
||||
setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
|
||||
|
||||
@@ -15,10 +15,10 @@ describe("file path helpers", () => {
|
||||
|
||||
test("normalizes Windows absolute paths with mixed separators", () => {
|
||||
const path = createPathHelpers(() => "C:\\repo")
|
||||
expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src/app.ts")
|
||||
expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src\\app.ts")
|
||||
expect(path.normalize("C:/repo/src/app.ts")).toBe("src/app.ts")
|
||||
expect(path.normalize("file://C:/repo/src/app.ts")).toBe("src/app.ts")
|
||||
expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src/app.ts")
|
||||
expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts")
|
||||
})
|
||||
|
||||
test("keeps query/hash stripping behavior stable", () => {
|
||||
|
||||
@@ -103,32 +103,30 @@ export function encodeFilePath(filepath: string): string {
|
||||
|
||||
export function createPathHelpers(scope: () => string) {
|
||||
const normalize = (input: string) => {
|
||||
const root = scope().replace(/\\/g, "/")
|
||||
const root = scope()
|
||||
|
||||
let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input)))).replace(/\\/g, "/")
|
||||
let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input))))
|
||||
|
||||
// Remove initial root prefix, if it's a complete match or followed by /
|
||||
// (don't want /foo/bar to root of /f).
|
||||
// For Windows paths, also check for case-insensitive match.
|
||||
const windows = /^[A-Za-z]:/.test(root)
|
||||
const canonRoot = windows ? root.toLowerCase() : root
|
||||
const canonPath = windows ? path.toLowerCase() : path
|
||||
// Separator-agnostic prefix stripping for Cygwin/native Windows compatibility
|
||||
// Only case-insensitive on Windows (drive letter or UNC paths)
|
||||
const windows = /^[A-Za-z]:/.test(root) || root.startsWith("\\\\")
|
||||
const canonRoot = windows ? root.replace(/\\/g, "/").toLowerCase() : root.replace(/\\/g, "/")
|
||||
const canonPath = windows ? path.replace(/\\/g, "/").toLowerCase() : path.replace(/\\/g, "/")
|
||||
if (
|
||||
canonPath.startsWith(canonRoot) &&
|
||||
(canonRoot.endsWith("/") || canonPath === canonRoot || canonPath.startsWith(canonRoot + "/"))
|
||||
(canonRoot.endsWith("/") || canonPath === canonRoot || canonPath[canonRoot.length] === "/")
|
||||
) {
|
||||
// If we match canonRoot + "/", the slash will be removed below.
|
||||
// Slice from original path to preserve native separators
|
||||
path = path.slice(root.length)
|
||||
}
|
||||
|
||||
if (path.startsWith("./")) {
|
||||
if (path.startsWith("./") || path.startsWith(".\\")) {
|
||||
path = path.slice(2)
|
||||
}
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
if (path.startsWith("/") || path.startsWith("\\")) {
|
||||
path = path.slice(1)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const MAX_FILE_VIEW_SESSIONS = 20
|
||||
const MAX_VIEW_FILES = 500
|
||||
|
||||
function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange {
|
||||
if (range.start <= range.end) return range
|
||||
if (range.start <= range.end) return { ...range }
|
||||
|
||||
const startSide = range.side
|
||||
const endSide = range.endSide ?? startSide
|
||||
|
||||
@@ -49,9 +49,12 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
let queue: Queued[] = []
|
||||
let buffer: Queued[] = []
|
||||
const coalesced = new Map<string, number>()
|
||||
const staleDeltas = new Set<string>()
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
let last = 0
|
||||
|
||||
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
|
||||
|
||||
const key = (directory: string, payload: Event) => {
|
||||
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
|
||||
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
|
||||
@@ -68,14 +71,20 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
if (queue.length === 0) return
|
||||
|
||||
const events = queue
|
||||
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
|
||||
queue = buffer
|
||||
buffer = events
|
||||
queue.length = 0
|
||||
coalesced.clear()
|
||||
staleDeltas.clear()
|
||||
|
||||
last = Date.now()
|
||||
batch(() => {
|
||||
for (const event of events) {
|
||||
if (skip && event.payload.type === "message.part.delta") {
|
||||
const props = event.payload.properties
|
||||
if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
|
||||
}
|
||||
emitter.emit(event.directory, event.payload)
|
||||
}
|
||||
})
|
||||
@@ -144,6 +153,10 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
const i = coalesced.get(k)
|
||||
if (i !== undefined) {
|
||||
queue[i] = { directory, payload }
|
||||
if (payload.type === "message.part.updated") {
|
||||
const part = payload.properties.part
|
||||
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
||||
}
|
||||
continue
|
||||
}
|
||||
coalesced.set(k, queue.length)
|
||||
|
||||
@@ -36,6 +36,7 @@ import type { ProjectMeta } from "./global-sync/types"
|
||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||
import { sanitizeProject } from "./global-sync/utils"
|
||||
import { usePlatform } from "./platform"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -51,12 +52,6 @@ type GlobalStore = {
|
||||
reload: undefined | "pending" | "complete"
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
if (error instanceof Error && error.message) return error.message
|
||||
if (typeof error === "string" && error) return error
|
||||
return "Unknown error"
|
||||
}
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
@@ -207,8 +202,9 @@ function createGlobalSync() {
|
||||
console.error("Failed to load sessions", err)
|
||||
const project = getFilename(directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.listFailed.title", { project }),
|
||||
description: errorMessage(err),
|
||||
description: formatServerError(err),
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { batch } from "solid-js"
|
||||
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import type { State, VcsCache } from "./types"
|
||||
import { cmp, normalizeProviderList } from "./utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -133,8 +134,11 @@ export async function bootstrapDirectory(input: {
|
||||
} catch (err) {
|
||||
console.error("Failed to bootstrap instance", err)
|
||||
const project = getFilename(input.directory)
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: `Failed to reload ${project}`, description: message })
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: `Failed to reload ${project}`,
|
||||
description: formatServerError(err),
|
||||
})
|
||||
input.setStore("status", "partial")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,4 +41,24 @@ describe("createScrollPersistence", () => {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
test("reseeds empty cache after persisted snapshot loads", () => {
|
||||
const snapshot = {
|
||||
session: {},
|
||||
} as Record<string, Record<string, { x: number; y: number }>>
|
||||
|
||||
const scroll = createScrollPersistence({
|
||||
getSnapshot: (sessionKey) => snapshot[sessionKey],
|
||||
onFlush: () => {},
|
||||
})
|
||||
|
||||
expect(scroll.scroll("session", "review")).toBeUndefined()
|
||||
|
||||
snapshot.session = {
|
||||
review: { x: 12, y: 34 },
|
||||
}
|
||||
|
||||
expect(scroll.scroll("session", "review")).toEqual({ x: 12, y: 34 })
|
||||
scroll.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,8 +33,16 @@ export function createScrollPersistence(opts: Options) {
|
||||
}
|
||||
|
||||
function seed(sessionKey: string) {
|
||||
if (cache[sessionKey]) return
|
||||
setCache(sessionKey, clone(opts.getSnapshot(sessionKey)))
|
||||
const next = clone(opts.getSnapshot(sessionKey))
|
||||
const current = cache[sessionKey]
|
||||
if (!current) {
|
||||
setCache(sessionKey, next)
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.keys(current).length > 0) return
|
||||
if (Object.keys(next).length === 0) return
|
||||
setCache(sessionKey, next)
|
||||
}
|
||||
|
||||
function scroll(sessionKey: string, tab: string) {
|
||||
|
||||
42
packages/app/src/context/permission-auto-respond.test.ts
Normal file
42
packages/app/src/context/permission-auto-respond.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { autoRespondsPermission } from "./permission-auto-respond"
|
||||
|
||||
const session = (input: { id: string; parentID?: string }) =>
|
||||
({
|
||||
id: input.id,
|
||||
parentID: input.parentID,
|
||||
}) as Session
|
||||
|
||||
const permission = (sessionID: string) =>
|
||||
({
|
||||
sessionID,
|
||||
}) as Pick<PermissionRequest, "sessionID">
|
||||
|
||||
describe("autoRespondsPermission", () => {
|
||||
test("uses a parent session's directory-scoped auto-accept", () => {
|
||||
const directory = "/tmp/project"
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
const autoAccept = {
|
||||
[`${base64Encode(directory)}/root`]: true,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
|
||||
})
|
||||
|
||||
test("uses a parent session's legacy auto-accept key", () => {
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
|
||||
expect(autoRespondsPermission({ root: true }, sessions, permission("child"), "/tmp/project")).toBe(true)
|
||||
})
|
||||
|
||||
test("ignores auto-accept from unrelated sessions", () => {
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" }), session({ id: "other" })]
|
||||
const autoAccept = {
|
||||
other: true,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), "/tmp/project")).toBe(false)
|
||||
})
|
||||
})
|
||||
36
packages/app/src/context/permission-auto-respond.ts
Normal file
36
packages/app/src/context/permission-auto-respond.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
|
||||
export function acceptKey(sessionID: string, directory?: string) {
|
||||
if (!directory) return sessionID
|
||||
return `${base64Encode(directory)}/${sessionID}`
|
||||
}
|
||||
|
||||
function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) {
|
||||
const parent = session.reduce((acc, item) => {
|
||||
if (item.parentID) acc.set(item.id, item.parentID)
|
||||
return acc
|
||||
}, new Map<string, string>())
|
||||
const seen = new Set([sessionID])
|
||||
const ids = [sessionID]
|
||||
|
||||
for (const id of ids) {
|
||||
const parentID = parent.get(id)
|
||||
if (!parentID || seen.has(parentID)) continue
|
||||
seen.add(parentID)
|
||||
ids.push(parentID)
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
export function autoRespondsPermission(
|
||||
autoAccept: Record<string, boolean>,
|
||||
session: { id: string; parentID?: string }[],
|
||||
permission: { sessionID: string },
|
||||
directory?: string,
|
||||
) {
|
||||
return sessionLineage(session, permission.sessionID).some((id) => {
|
||||
const key = acceptKey(id, directory)
|
||||
return autoAccept[key] ?? autoAccept[id] ?? false
|
||||
})
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import { Persist, persisted } from "@/utils/persist"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { acceptKey, autoRespondsPermission } from "./permission-auto-respond"
|
||||
|
||||
type PermissionRespondFn = (input: {
|
||||
sessionID: string
|
||||
@@ -16,10 +16,6 @@ type PermissionRespondFn = (input: {
|
||||
directory?: string
|
||||
}) => void
|
||||
|
||||
function shouldAutoAccept(perm: PermissionRequest) {
|
||||
return perm.permission === "edit"
|
||||
}
|
||||
|
||||
function isNonAllowRule(rule: unknown) {
|
||||
if (!rule) return false
|
||||
if (typeof rule === "string") return rule !== "allow"
|
||||
@@ -40,10 +36,7 @@ function hasPermissionPromptRules(permission: unknown) {
|
||||
if (Array.isArray(permission)) return false
|
||||
|
||||
const config = permission as Record<string, unknown>
|
||||
if (isNonAllowRule(config.edit)) return true
|
||||
if (isNonAllowRule(config.write)) return true
|
||||
|
||||
return false
|
||||
return Object.values(config).some(isNonAllowRule)
|
||||
}
|
||||
|
||||
export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
|
||||
@@ -61,9 +54,25 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
})
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("permission", ["permission.v3"]),
|
||||
{
|
||||
...Persist.global("permission", ["permission.v3"]),
|
||||
migrate(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return value
|
||||
|
||||
const data = value as Record<string, unknown>
|
||||
if (data.autoAccept) return value
|
||||
|
||||
return {
|
||||
...data,
|
||||
autoAccept:
|
||||
typeof data.autoAcceptEdits === "object" && data.autoAcceptEdits && !Array.isArray(data.autoAcceptEdits)
|
||||
? data.autoAcceptEdits
|
||||
: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
createStore({
|
||||
autoAcceptEdits: {} as Record<string, boolean>,
|
||||
autoAccept: {} as Record<string, boolean>,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -105,14 +114,14 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
})
|
||||
}
|
||||
|
||||
function acceptKey(sessionID: string, directory?: string) {
|
||||
if (!directory) return sessionID
|
||||
return `${base64Encode(directory)}/${sessionID}`
|
||||
}
|
||||
|
||||
function isAutoAccepting(sessionID: string, directory?: string) {
|
||||
const key = acceptKey(sessionID, directory)
|
||||
return store.autoAcceptEdits[key] ?? store.autoAcceptEdits[sessionID] ?? false
|
||||
return store.autoAccept[key] ?? store.autoAccept[sessionID] ?? false
|
||||
}
|
||||
|
||||
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
|
||||
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
|
||||
return autoRespondsPermission(store.autoAccept, session, permission, directory)
|
||||
}
|
||||
|
||||
function bumpEnableVersion(sessionID: string, directory?: string) {
|
||||
@@ -127,8 +136,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
if (event?.type !== "permission.asked") return
|
||||
|
||||
const perm = event.properties
|
||||
if (!isAutoAccepting(perm.sessionID, e.name)) return
|
||||
if (!shouldAutoAccept(perm)) return
|
||||
if (!shouldAutoRespond(perm, e.name)) return
|
||||
|
||||
respondOnce(perm, e.name)
|
||||
})
|
||||
@@ -139,8 +147,8 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
const version = bumpEnableVersion(sessionID, directory)
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.autoAcceptEdits[key] = true
|
||||
delete draft.autoAcceptEdits[sessionID]
|
||||
draft.autoAccept[key] = true
|
||||
delete draft.autoAccept[sessionID]
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -151,8 +159,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
if (!isAutoAccepting(sessionID, directory)) return
|
||||
for (const perm of x.data ?? []) {
|
||||
if (!perm?.id) continue
|
||||
if (perm.sessionID !== sessionID) continue
|
||||
if (!shouldAutoAccept(perm)) continue
|
||||
if (!shouldAutoRespond(perm, directory)) continue
|
||||
respondOnce(perm, directory)
|
||||
}
|
||||
})
|
||||
@@ -164,8 +171,8 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
const key = directory ? acceptKey(sessionID, directory) : undefined
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
if (key) delete draft.autoAcceptEdits[key]
|
||||
delete draft.autoAcceptEdits[sessionID]
|
||||
if (key) delete draft.autoAccept[key]
|
||||
delete draft.autoAccept[sessionID]
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -174,7 +181,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
ready,
|
||||
respond,
|
||||
autoResponds(permission: PermissionRequest, directory?: string) {
|
||||
return isAutoAccepting(permission.sessionID, directory) && shouldAutoAccept(permission)
|
||||
return shouldAutoRespond(permission, directory)
|
||||
},
|
||||
isAutoAccepting,
|
||||
toggleAutoAccept(sessionID: string, directory: string) {
|
||||
|
||||
@@ -116,6 +116,10 @@ function contextItemKey(item: ContextItem) {
|
||||
return `${key}:c=${digest.slice(0, 8)}`
|
||||
}
|
||||
|
||||
function isCommentItem(item: ContextItem | (ContextItem & { key: string })) {
|
||||
return item.type === "file" && !!item.comment?.trim()
|
||||
}
|
||||
|
||||
function createPromptActions(
|
||||
setStore: SetStoreFunction<{
|
||||
prompt: Prompt
|
||||
@@ -189,6 +193,26 @@ function createPromptSession(dir: string, id: string | undefined) {
|
||||
remove(key: string) {
|
||||
setStore("context", "items", (items) => items.filter((x) => x.key !== key))
|
||||
},
|
||||
removeComment(path: string, commentID: string) {
|
||||
setStore("context", "items", (items) =>
|
||||
items.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)),
|
||||
)
|
||||
},
|
||||
updateComment(path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) {
|
||||
setStore("context", "items", (items) =>
|
||||
items.map((item) => {
|
||||
if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item
|
||||
const value = { ...item, ...next }
|
||||
return { ...value, key: contextItemKey(value) }
|
||||
}),
|
||||
)
|
||||
},
|
||||
replaceComments(items: FileContextItem[]) {
|
||||
setStore("context", "items", (current) => [
|
||||
...current.filter((item) => !isCommentItem(item)),
|
||||
...items.map((item) => ({ ...item, key: contextItemKey(item) })),
|
||||
])
|
||||
},
|
||||
},
|
||||
set: actions.set,
|
||||
reset: actions.reset,
|
||||
@@ -251,6 +275,10 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
||||
items: () => session().context.items(),
|
||||
add: (item: ContextItem) => session().context.add(item),
|
||||
remove: (key: string) => session().context.remove(key),
|
||||
removeComment: (path: string, commentID: string) => session().context.removeComment(path, commentID),
|
||||
updateComment: (path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) =>
|
||||
session().context.updateComment(path, commentID, next),
|
||||
replaceComments: (items: FileContextItem[]) => session().context.replaceComments(items),
|
||||
},
|
||||
set: (prompt: Prompt, cursorPosition?: number) => session().set(prompt, cursorPosition),
|
||||
reset: () => session().reset(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Persist, persisted } from "@/utils/persist"
|
||||
import { checkServerHealth } from "@/utils/server-health"
|
||||
|
||||
type StoredProject = { worktree: string; expanded: boolean }
|
||||
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
||||
const HEALTH_POLL_INTERVAL_MS = 10_000
|
||||
|
||||
export function normalizeServerUrl(input: string) {
|
||||
@@ -15,9 +16,9 @@ export function normalizeServerUrl(input: string) {
|
||||
return withProtocol.replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
export function serverDisplayName(conn?: ServerConnection.Any) {
|
||||
export function serverName(conn?: ServerConnection.Any, ignoreDisplayName = false) {
|
||||
if (!conn) return ""
|
||||
if (conn.displayName) return conn.displayName
|
||||
if (conn.displayName && !ignoreDisplayName) return conn.displayName
|
||||
return conn.http.url.replace(/^https?:\/\//, "").replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
@@ -100,22 +101,33 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("server", ["server.v3"]),
|
||||
createStore({
|
||||
list: [] as string[],
|
||||
list: [] as StoredServer[],
|
||||
projects: {} as Record<string, StoredProject[]>,
|
||||
lastProject: {} as Record<string, string>,
|
||||
}),
|
||||
)
|
||||
|
||||
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
|
||||
|
||||
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
||||
const servers = [
|
||||
...(props.servers ?? []),
|
||||
...store.list.map((value) => ({
|
||||
type: "http" as const,
|
||||
http: typeof value === "string" ? { url: value } : value,
|
||||
})),
|
||||
...store.list.map((value) =>
|
||||
typeof value === "string"
|
||||
? {
|
||||
type: "http" as const,
|
||||
http: { url: value },
|
||||
}
|
||||
: value,
|
||||
),
|
||||
]
|
||||
|
||||
const deduped = new Map(servers.map((conn) => [ServerConnection.key(conn), conn]))
|
||||
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()]
|
||||
})
|
||||
@@ -156,27 +168,29 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
if (state.active !== input) setState("active", input)
|
||||
}
|
||||
|
||||
function add(input: string) {
|
||||
const url = normalizeServerUrl(input)
|
||||
if (!url) return
|
||||
function add(input: ServerConnection.Http) {
|
||||
const url_ = normalizeServerUrl(input.http.url)
|
||||
if (!url_) return
|
||||
const conn = { ...input, http: { ...input.http, url: url_ } }
|
||||
return batch(() => {
|
||||
const http: ServerConnection.HttpBase = { url }
|
||||
if (!store.list.includes(url)) {
|
||||
setStore("list", store.list.length, url)
|
||||
const existing = store.list.findIndex((x) => url(x) === url_)
|
||||
if (existing !== -1) {
|
||||
setStore("list", existing, conn)
|
||||
} else {
|
||||
setStore("list", store.list.length, conn)
|
||||
}
|
||||
const conn: ServerConnection.Http = { type: "http", http }
|
||||
setState("active", ServerConnection.key(conn))
|
||||
return conn
|
||||
})
|
||||
}
|
||||
|
||||
function remove(key: ServerConnection.Key) {
|
||||
const list = store.list.filter((x) => x !== key)
|
||||
const list = store.list.filter((x) => url(x) !== key)
|
||||
batch(() => {
|
||||
setStore("list", list)
|
||||
if (state.active === key) {
|
||||
const next = list[0]
|
||||
setState("active", next ? ServerConnection.key({ type: "http", http: { url: next } }) : props.defaultServer)
|
||||
setState("active", next ? ServerConnection.Key.make(url(next)) : props.defaultServer)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -212,7 +226,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
return state.active
|
||||
},
|
||||
get name() {
|
||||
return serverDisplayName(current())
|
||||
return serverName(current())
|
||||
},
|
||||
get list() {
|
||||
return allServers()
|
||||
|
||||
@@ -3,7 +3,16 @@ import { decode64 } from "@/utils/base64"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo } from "solid-js"
|
||||
|
||||
export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
|
||||
export const popularProviders = [
|
||||
"opencode",
|
||||
"opencode-go",
|
||||
"anthropic",
|
||||
"github-copilot",
|
||||
"openai",
|
||||
"google",
|
||||
"openrouter",
|
||||
"vercel",
|
||||
]
|
||||
const popularProviderSet = new Set(popularProviders)
|
||||
|
||||
export function useProviders() {
|
||||
|
||||
@@ -65,8 +65,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
||||
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
||||
"command.permissions.autoaccept.enable": "قبول الأذونات تلقائيًا",
|
||||
"command.permissions.autoaccept.disable": "إيقاف قبول الأذونات تلقائيًا",
|
||||
"command.workspace.toggle": "تبديل مساحات العمل",
|
||||
"command.workspace.toggle.description": "تمكين أو تعطيل مساحات العمل المتعددة في الشريط الجانبي",
|
||||
"command.session.undo": "تراجع",
|
||||
@@ -91,6 +91,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "آخر",
|
||||
"dialog.provider.tag.recommended": "موصى به",
|
||||
"dialog.provider.opencode.note": "نماذج مختارة تتضمن Claude و GPT و Gemini والمزيد",
|
||||
"dialog.provider.opencode.tagline": "نماذج موثوقة ومحسنة",
|
||||
"dialog.provider.opencodeGo.tagline": "اشتراك منخفض التكلفة للجميع",
|
||||
"dialog.provider.anthropic.note": "اتصل باستخدام Claude Pro/Max أو مفتاح API",
|
||||
"dialog.provider.copilot.note": "اتصل باستخدام Copilot أو مفتاح API",
|
||||
"dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API",
|
||||
@@ -364,10 +366,10 @@ export const dict = {
|
||||
"toast.workspace.enabled.description": "الآن يتم عرض عدة worktrees في الشريط الجانبي",
|
||||
"toast.workspace.disabled.title": "تم تعطيل مساحات العمل",
|
||||
"toast.workspace.disabled.description": "يتم عرض worktree الرئيسي فقط في الشريط الجانبي",
|
||||
"toast.permissions.autoaccept.on.title": "قبول التعديلات تلقائيًا",
|
||||
"toast.permissions.autoaccept.on.description": "سيتم الموافقة تلقائيًا على أذونات التحرير والكتابة",
|
||||
"toast.permissions.autoaccept.off.title": "توقف قبول التعديلات تلقائيًا",
|
||||
"toast.permissions.autoaccept.off.description": "ستتطلب أذونات التحرير والكتابة موافقة",
|
||||
"toast.permissions.autoaccept.on.title": "يتم قبول الأذونات تلقائيًا",
|
||||
"toast.permissions.autoaccept.on.description": "ستتم الموافقة على طلبات الأذونات تلقائيًا",
|
||||
"toast.permissions.autoaccept.off.title": "تم إيقاف قبول الأذونات تلقائيًا",
|
||||
"toast.permissions.autoaccept.off.description": "ستتطلب طلبات الأذونات موافقة",
|
||||
"toast.model.none.title": "لم يتم تحديد نموذج",
|
||||
"toast.model.none.description": "قم بتوصيل موفر لتلخيص هذه الجلسة",
|
||||
"toast.file.loadFailed.title": "فشل تحميل الملف",
|
||||
|
||||
@@ -65,8 +65,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
|
||||
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
|
||||
"command.permissions.autoaccept.enable": "Aceitar permissões automaticamente",
|
||||
"command.permissions.autoaccept.disable": "Parar de aceitar permissões automaticamente",
|
||||
"command.workspace.toggle": "Alternar espaços de trabalho",
|
||||
"command.workspace.toggle.description": "Habilitar ou desabilitar múltiplos espaços de trabalho na barra lateral",
|
||||
"command.session.undo": "Desfazer",
|
||||
@@ -91,6 +91,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Outro",
|
||||
"dialog.provider.tag.recommended": "Recomendado",
|
||||
"dialog.provider.opencode.note": "Modelos selecionados incluindo Claude, GPT, Gemini e mais",
|
||||
"dialog.provider.opencode.tagline": "Modelos otimizados e confiáveis",
|
||||
"dialog.provider.opencodeGo.tagline": "Assinatura de baixo custo para todos",
|
||||
"dialog.provider.anthropic.note": "Conectar com Claude Pro/Max ou chave de API",
|
||||
"dialog.provider.copilot.note": "Conectar com Copilot ou chave de API",
|
||||
"dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API",
|
||||
@@ -365,10 +367,10 @@ export const dict = {
|
||||
"toast.workspace.enabled.description": "Várias worktrees agora são exibidas na barra lateral",
|
||||
"toast.workspace.disabled.title": "Espaços de trabalho desativados",
|
||||
"toast.workspace.disabled.description": "Apenas a worktree principal é exibida na barra lateral",
|
||||
"toast.permissions.autoaccept.on.title": "Aceitando edições automaticamente",
|
||||
"toast.permissions.autoaccept.on.description": "Permissões de edição e escrita serão aprovadas automaticamente",
|
||||
"toast.permissions.autoaccept.off.title": "Parou de aceitar edições automaticamente",
|
||||
"toast.permissions.autoaccept.off.description": "Permissões de edição e escrita exigirão aprovação",
|
||||
"toast.permissions.autoaccept.on.title": "Aceitando permissões automaticamente",
|
||||
"toast.permissions.autoaccept.on.description": "Solicitações de permissão serão aprovadas automaticamente",
|
||||
"toast.permissions.autoaccept.off.title": "Parou de aceitar permissões automaticamente",
|
||||
"toast.permissions.autoaccept.off.description": "Solicitações de permissão exigirão aprovação",
|
||||
"toast.model.none.title": "Nenhum modelo selecionado",
|
||||
"toast.model.none.description": "Conecte um provedor para resumir esta sessão",
|
||||
"toast.file.loadFailed.title": "Falha ao carregar arquivo",
|
||||
|
||||
@@ -71,8 +71,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Prebaci na sljedeći nivo",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
|
||||
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
|
||||
"command.permissions.autoaccept.enable": "Automatski prihvati dozvole",
|
||||
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje dozvola",
|
||||
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
|
||||
"command.workspace.toggle.description": "Omogući ili onemogući više radnih prostora u bočnoj traci",
|
||||
"command.session.undo": "Poništi",
|
||||
@@ -99,8 +99,10 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Ostalo",
|
||||
"dialog.provider.tag.recommended": "Preporučeno",
|
||||
"dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge",
|
||||
"dialog.provider.opencode.tagline": "Pouzdani optimizovani modeli",
|
||||
"dialog.provider.opencodeGo.tagline": "Povoljna pretplata za sve",
|
||||
"dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max",
|
||||
"dialog.provider.copilot.note": "Claude modeli za pomoć pri kodiranju",
|
||||
"dialog.provider.copilot.note": "AI modeli za pomoć pri kodiranju putem GitHub Copilot",
|
||||
"dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke",
|
||||
"dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore",
|
||||
"dialog.provider.openrouter.note": "Pristup svim podržanim modelima preko jednog provajdera",
|
||||
@@ -403,10 +405,10 @@ export const dict = {
|
||||
"toast.workspace.disabled.title": "Radni prostori onemogućeni",
|
||||
"toast.workspace.disabled.description": "Samo glavni worktree se prikazuje u bočnoj traci",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Automatsko prihvatanje izmjena",
|
||||
"toast.permissions.autoaccept.on.description": "Dozvole za izmjene i pisanje biće automatski odobrene",
|
||||
"toast.permissions.autoaccept.off.title": "Zaustavljeno automatsko prihvatanje izmjena",
|
||||
"toast.permissions.autoaccept.off.description": "Dozvole za izmjene i pisanje zahtijevaće odobrenje",
|
||||
"toast.permissions.autoaccept.on.title": "Automatsko prihvatanje dozvola",
|
||||
"toast.permissions.autoaccept.on.description": "Zahtjevi za dozvole će biti automatski odobreni",
|
||||
"toast.permissions.autoaccept.off.title": "Zaustavljeno automatsko prihvatanje dozvola",
|
||||
"toast.permissions.autoaccept.off.description": "Zahtjevi za dozvole će zahtijevati odobrenje",
|
||||
|
||||
"toast.model.none.title": "Nije odabran model",
|
||||
"toast.model.none.description": "Poveži provajdera da sažmeš ovu sesiju",
|
||||
|
||||
@@ -71,8 +71,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
||||
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
||||
"command.permissions.autoaccept.enable": "Accepter tilladelser automatisk",
|
||||
"command.permissions.autoaccept.disable": "Stop med at acceptere tilladelser automatisk",
|
||||
"command.workspace.toggle": "Skift arbejdsområder",
|
||||
"command.workspace.toggle.description": "Aktiver eller deaktiver flere arbejdsområder i sidebjælken",
|
||||
"command.session.undo": "Fortryd",
|
||||
@@ -99,8 +99,10 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Andre",
|
||||
"dialog.provider.tag.recommended": "Anbefalet",
|
||||
"dialog.provider.opencode.note": "Udvalgte modeller inklusive Claude, GPT, Gemini og flere",
|
||||
"dialog.provider.opencode.tagline": "Pålidelige optimerede modeller",
|
||||
"dialog.provider.opencodeGo.tagline": "Billigt abonnement for alle",
|
||||
"dialog.provider.anthropic.note": "Direkte adgang til Claude-modeller, inklusive Pro og Max",
|
||||
"dialog.provider.copilot.note": "Claude-modeller til kodningsassistance",
|
||||
"dialog.provider.copilot.note": "AI-modeller til kodningsassistance via GitHub Copilot",
|
||||
"dialog.provider.openai.note": "GPT-modeller til hurtige, kompetente generelle AI-opgaver",
|
||||
"dialog.provider.google.note": "Gemini-modeller til hurtige, strukturerede svar",
|
||||
"dialog.provider.openrouter.note": "Få adgang til alle understøttede modeller fra én udbyder",
|
||||
@@ -396,10 +398,10 @@ export const dict = {
|
||||
"toast.theme.title": "Tema skiftet",
|
||||
"toast.scheme.title": "Farveskema",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Accepterer ændringer automatisk",
|
||||
"toast.permissions.autoaccept.on.description": "Redigerings- og skrivetilladelser vil automatisk blive godkendt",
|
||||
"toast.permissions.autoaccept.off.title": "Stoppede automatisk accept af ændringer",
|
||||
"toast.permissions.autoaccept.off.description": "Redigerings- og skrivetilladelser vil kræve godkendelse",
|
||||
"toast.permissions.autoaccept.on.title": "Accepterer tilladelser automatisk",
|
||||
"toast.permissions.autoaccept.on.description": "Anmodninger om tilladelse godkendes automatisk",
|
||||
"toast.permissions.autoaccept.off.title": "Stoppet med at acceptere tilladelser automatisk",
|
||||
"toast.permissions.autoaccept.off.description": "Anmodninger om tilladelse vil kræve godkendelse",
|
||||
|
||||
"toast.workspace.enabled.title": "Arbejdsområder aktiveret",
|
||||
"toast.workspace.enabled.description": "Flere worktrees vises nu i sidepanelet",
|
||||
|
||||
@@ -69,8 +69,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
||||
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
||||
"command.permissions.autoaccept.enable": "Berechtigungen automatisch akzeptieren",
|
||||
"command.permissions.autoaccept.disable": "Automatische Akzeptanz von Berechtigungen stoppen",
|
||||
"command.workspace.toggle": "Arbeitsbereiche umschalten",
|
||||
"command.workspace.toggle.description": "Mehrere Arbeitsbereiche in der Seitenleiste aktivieren oder deaktivieren",
|
||||
"command.session.undo": "Rückgängig",
|
||||
@@ -95,6 +95,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Andere",
|
||||
"dialog.provider.tag.recommended": "Empfohlen",
|
||||
"dialog.provider.opencode.note": "Kuratierte Modelle inklusive Claude, GPT, Gemini und mehr",
|
||||
"dialog.provider.opencode.tagline": "Zuverlässige, optimierte Modelle",
|
||||
"dialog.provider.opencodeGo.tagline": "Kostengünstiges Abo für alle",
|
||||
"dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden",
|
||||
"dialog.provider.copilot.note": "Mit Copilot oder API-Schlüssel verbinden",
|
||||
"dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden",
|
||||
@@ -372,10 +374,10 @@ export const dict = {
|
||||
"toast.workspace.enabled.description": "Mehrere Worktrees werden jetzt in der Seitenleiste angezeigt",
|
||||
"toast.workspace.disabled.title": "Arbeitsbereiche deaktiviert",
|
||||
"toast.workspace.disabled.description": "Nur der Haupt-Worktree wird in der Seitenleiste angezeigt",
|
||||
"toast.permissions.autoaccept.on.title": "Änderungen werden automatisch akzeptiert",
|
||||
"toast.permissions.autoaccept.on.description": "Bearbeitungs- und Schreibrechte werden automatisch genehmigt",
|
||||
"toast.permissions.autoaccept.off.title": "Automatische Annahme von Änderungen gestoppt",
|
||||
"toast.permissions.autoaccept.off.description": "Bearbeitungs- und Schreibrechte erfordern Genehmigung",
|
||||
"toast.permissions.autoaccept.on.title": "Berechtigungen werden automatisch akzeptiert",
|
||||
"toast.permissions.autoaccept.on.description": "Berechtigungsanfragen werden automatisch genehmigt",
|
||||
"toast.permissions.autoaccept.off.title": "Automatische Akzeptanz von Berechtigungen gestoppt",
|
||||
"toast.permissions.autoaccept.off.description": "Berechtigungsanfragen erfordern eine Genehmigung",
|
||||
"toast.model.none.title": "Kein Modell ausgewählt",
|
||||
"toast.model.none.description": "Verbinden Sie einen Anbieter, um diese Sitzung zusammenzufassen",
|
||||
"toast.file.loadFailed.title": "Datei konnte nicht geladen werden",
|
||||
|
||||
@@ -71,8 +71,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Switch to the next effort level",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
||||
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
|
||||
"command.permissions.autoaccept.enable": "Auto-accept permissions",
|
||||
"command.permissions.autoaccept.disable": "Stop auto-accepting permissions",
|
||||
"command.workspace.toggle": "Toggle workspaces",
|
||||
"command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar",
|
||||
"command.session.undo": "Undo",
|
||||
@@ -99,8 +99,10 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Other",
|
||||
"dialog.provider.tag.recommended": "Recommended",
|
||||
"dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more",
|
||||
"dialog.provider.opencode.tagline": "Reliable optimized models",
|
||||
"dialog.provider.opencodeGo.tagline": "Low cost subscription for everyone",
|
||||
"dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max",
|
||||
"dialog.provider.copilot.note": "Claude models for coding assistance",
|
||||
"dialog.provider.copilot.note": "AI models for coding assistance via GitHub Copilot",
|
||||
"dialog.provider.openai.note": "GPT models for fast, capable general AI tasks",
|
||||
"dialog.provider.google.note": "Gemini models for fast, structured responses",
|
||||
"dialog.provider.openrouter.note": "Access all supported models from one provider",
|
||||
@@ -307,12 +309,17 @@ export const dict = {
|
||||
"dialog.server.description": "Switch which OpenCode server this app connects to.",
|
||||
"dialog.server.search.placeholder": "Search servers",
|
||||
"dialog.server.empty": "No servers yet",
|
||||
"dialog.server.add.title": "Add a server",
|
||||
"dialog.server.add.url": "Server URL",
|
||||
"dialog.server.add.title": "Add server",
|
||||
"dialog.server.add.url": "Server address",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "Could not connect to server",
|
||||
"dialog.server.add.checking": "Checking...",
|
||||
"dialog.server.add.button": "Add server",
|
||||
"dialog.server.add.name": "Server name (optional)",
|
||||
"dialog.server.add.namePlaceholder": "Localhost",
|
||||
"dialog.server.add.username": "Username (optional)",
|
||||
"dialog.server.add.password": "Password (optional)",
|
||||
"dialog.server.edit.title": "Edit server",
|
||||
"dialog.server.default.title": "Default server",
|
||||
"dialog.server.default.description":
|
||||
"Connect to this server on app launch instead of starting a local server. Requires restart.",
|
||||
@@ -402,10 +409,10 @@ export const dict = {
|
||||
"toast.workspace.disabled.title": "Workspaces disabled",
|
||||
"toast.workspace.disabled.description": "Only the main worktree is shown in the sidebar",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Auto-accepting edits",
|
||||
"toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved",
|
||||
"toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits",
|
||||
"toast.permissions.autoaccept.off.description": "Edit and write permissions will require approval",
|
||||
"toast.permissions.autoaccept.on.title": "Auto-accepting permissions",
|
||||
"toast.permissions.autoaccept.on.description": "Permission requests will be automatically approved",
|
||||
"toast.permissions.autoaccept.off.title": "Stopped auto-accepting permissions",
|
||||
"toast.permissions.autoaccept.off.description": "Permission requests will require approval",
|
||||
|
||||
"toast.model.none.title": "No model selected",
|
||||
"toast.model.none.description": "Connect a provider to summarize this session",
|
||||
|
||||
@@ -71,8 +71,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
||||
"command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente",
|
||||
"command.permissions.autoaccept.enable": "Aceptar permisos automáticamente",
|
||||
"command.permissions.autoaccept.disable": "Dejar de aceptar permisos automáticamente",
|
||||
"command.workspace.toggle": "Alternar espacios de trabajo",
|
||||
"command.workspace.toggle.description": "Habilitar o deshabilitar múltiples espacios de trabajo en la barra lateral",
|
||||
"command.session.undo": "Deshacer",
|
||||
@@ -99,8 +99,10 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Otro",
|
||||
"dialog.provider.tag.recommended": "Recomendado",
|
||||
"dialog.provider.opencode.note": "Modelos seleccionados incluyendo Claude, GPT, Gemini y más",
|
||||
"dialog.provider.opencode.tagline": "Modelos optimizados y fiables",
|
||||
"dialog.provider.opencodeGo.tagline": "Suscripción económica para todos",
|
||||
"dialog.provider.anthropic.note": "Acceso directo a modelos Claude, incluyendo Pro y Max",
|
||||
"dialog.provider.copilot.note": "Modelos Claude para asistencia de codificación",
|
||||
"dialog.provider.copilot.note": "Modelos de IA para asistencia de codificación a través de GitHub Copilot",
|
||||
"dialog.provider.openai.note": "Modelos GPT para tareas de IA generales rápidas y capaces",
|
||||
"dialog.provider.google.note": "Modelos Gemini para respuestas rápidas y estructuradas",
|
||||
"dialog.provider.openrouter.note": "Accede a todos los modelos soportados desde un solo proveedor",
|
||||
@@ -403,10 +405,10 @@ export const dict = {
|
||||
"toast.workspace.disabled.title": "Espacios de trabajo deshabilitados",
|
||||
"toast.workspace.disabled.description": "Solo se muestra el worktree principal en la barra lateral",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Aceptando ediciones automáticamente",
|
||||
"toast.permissions.autoaccept.on.description": "Los permisos de edición y escritura serán aprobados automáticamente",
|
||||
"toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente",
|
||||
"toast.permissions.autoaccept.off.description": "Los permisos de edición y escritura requerirán aprobación",
|
||||
"toast.permissions.autoaccept.on.title": "Aceptando permisos automáticamente",
|
||||
"toast.permissions.autoaccept.on.description": "Las solicitudes de permisos se aprobarán automáticamente",
|
||||
"toast.permissions.autoaccept.off.title": "Se dejó de aceptar permisos automáticamente",
|
||||
"toast.permissions.autoaccept.off.description": "Las solicitudes de permisos requerirán aprobación",
|
||||
|
||||
"toast.model.none.title": "Ningún modelo seleccionado",
|
||||
"toast.model.none.description": "Conecta un proveedor para resumir esta sesión",
|
||||
|
||||
@@ -65,8 +65,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Passer au niveau d'effort suivant",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
||||
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
|
||||
"command.permissions.autoaccept.enable": "Accepter automatiquement les permissions",
|
||||
"command.permissions.autoaccept.disable": "Arrêter d'accepter automatiquement les permissions",
|
||||
"command.workspace.toggle": "Basculer les espaces de travail",
|
||||
"command.workspace.toggle.description": "Activer ou désactiver plusieurs espaces de travail dans la barre latérale",
|
||||
"command.session.undo": "Annuler",
|
||||
@@ -91,6 +91,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Autre",
|
||||
"dialog.provider.tag.recommended": "Recommandé",
|
||||
"dialog.provider.opencode.note": "Modèles sélectionnés incluant Claude, GPT, Gemini et plus",
|
||||
"dialog.provider.opencode.tagline": "Modèles optimisés et fiables",
|
||||
"dialog.provider.opencodeGo.tagline": "Abonnement abordable pour tous",
|
||||
"dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API",
|
||||
"dialog.provider.copilot.note": "Connectez-vous avec Copilot ou une clé API",
|
||||
"dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API",
|
||||
@@ -366,12 +368,10 @@ export const dict = {
|
||||
"toast.workspace.enabled.description": "Plusieurs worktrees sont désormais affichés dans la barre latérale",
|
||||
"toast.workspace.disabled.title": "Espaces de travail désactivés",
|
||||
"toast.workspace.disabled.description": "Seul le worktree principal est affiché dans la barre latérale",
|
||||
"toast.permissions.autoaccept.on.title": "Acceptation auto des modifications",
|
||||
"toast.permissions.autoaccept.on.description":
|
||||
"Les permissions de modification et d'écriture seront automatiquement approuvées",
|
||||
"toast.permissions.autoaccept.off.title": "Arrêt acceptation auto des modifications",
|
||||
"toast.permissions.autoaccept.off.description":
|
||||
"Les permissions de modification et d'écriture nécessiteront une approbation",
|
||||
"toast.permissions.autoaccept.on.title": "Acceptation automatique des permissions",
|
||||
"toast.permissions.autoaccept.on.description": "Les demandes de permission seront approuvées automatiquement",
|
||||
"toast.permissions.autoaccept.off.title": "Acceptation automatique des permissions arrêtée",
|
||||
"toast.permissions.autoaccept.off.description": "Les demandes de permission nécessiteront une approbation",
|
||||
"toast.model.none.title": "Aucun modèle sélectionné",
|
||||
"toast.model.none.description": "Connectez un fournisseur pour résumer cette session",
|
||||
"toast.file.loadFailed.title": "Échec du chargement du fichier",
|
||||
|
||||
@@ -65,8 +65,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
||||
"command.prompt.mode.shell": "シェル",
|
||||
"command.prompt.mode.normal": "プロンプト",
|
||||
"command.permissions.autoaccept.enable": "編集を自動承認",
|
||||
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
||||
"command.permissions.autoaccept.enable": "権限を自動承認する",
|
||||
"command.permissions.autoaccept.disable": "権限の自動承認を停止する",
|
||||
"command.workspace.toggle": "ワークスペースを切り替え",
|
||||
"command.workspace.toggle.description": "サイドバーでの複数のワークスペースの有効化・無効化",
|
||||
"command.session.undo": "元に戻す",
|
||||
@@ -91,6 +91,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "その他",
|
||||
"dialog.provider.tag.recommended": "推奨",
|
||||
"dialog.provider.opencode.note": "Claude, GPT, Geminiなどを含む厳選されたモデル",
|
||||
"dialog.provider.opencode.tagline": "信頼性の高い最適化モデル",
|
||||
"dialog.provider.opencodeGo.tagline": "すべての人に低価格のサブスクリプション",
|
||||
"dialog.provider.anthropic.note": "Claude Pro/MaxまたはAPIキーで接続",
|
||||
"dialog.provider.copilot.note": "CopilotまたはAPIキーで接続",
|
||||
"dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続",
|
||||
@@ -364,10 +366,10 @@ export const dict = {
|
||||
"toast.workspace.enabled.description": "サイドバーに複数のワークツリーが表示されます",
|
||||
"toast.workspace.disabled.title": "ワークスペースが無効になりました",
|
||||
"toast.workspace.disabled.description": "サイドバーにはメインのワークツリーのみが表示されます",
|
||||
"toast.permissions.autoaccept.on.title": "編集を自動承認中",
|
||||
"toast.permissions.autoaccept.on.description": "編集と書き込みの権限は自動的に承認されます",
|
||||
"toast.permissions.autoaccept.off.title": "編集の自動承認を停止しました",
|
||||
"toast.permissions.autoaccept.off.description": "編集と書き込みの権限には承認が必要です",
|
||||
"toast.permissions.autoaccept.on.title": "権限を自動承認しています",
|
||||
"toast.permissions.autoaccept.on.description": "権限の要求は自動的に承認されます",
|
||||
"toast.permissions.autoaccept.off.title": "権限の自動承認を停止しました",
|
||||
"toast.permissions.autoaccept.off.description": "権限の要求には承認が必要になります",
|
||||
"toast.model.none.title": "モデルが選択されていません",
|
||||
"toast.model.none.description": "このセッションを要約するにはプロバイダーを接続してください",
|
||||
"toast.file.loadFailed.title": "ファイルの読み込みに失敗しました",
|
||||
|
||||
@@ -69,8 +69,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
||||
"command.prompt.mode.shell": "셸",
|
||||
"command.prompt.mode.normal": "프롬프트",
|
||||
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
||||
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
||||
"command.permissions.autoaccept.enable": "권한 자동 수락",
|
||||
"command.permissions.autoaccept.disable": "권한 자동 수락 중지",
|
||||
"command.workspace.toggle": "작업 공간 전환",
|
||||
"command.workspace.toggle.description": "사이드바에서 다중 작업 공간 활성화 또는 비활성화",
|
||||
"command.session.undo": "실행 취소",
|
||||
@@ -95,6 +95,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "기타",
|
||||
"dialog.provider.tag.recommended": "추천",
|
||||
"dialog.provider.opencode.note": "Claude, GPT, Gemini 등을 포함한 엄선된 모델",
|
||||
"dialog.provider.opencode.tagline": "신뢰할 수 있는 최적화 모델",
|
||||
"dialog.provider.opencodeGo.tagline": "모두를 위한 저렴한 구독",
|
||||
"dialog.provider.anthropic.note": "Claude Pro/Max 또는 API 키로 연결",
|
||||
"dialog.provider.copilot.note": "Copilot 또는 API 키로 연결",
|
||||
"dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결",
|
||||
@@ -367,10 +369,10 @@ export const dict = {
|
||||
"toast.workspace.enabled.description": "이제 사이드바에 여러 작업 트리가 표시됩니다",
|
||||
"toast.workspace.disabled.title": "작업 공간 비활성화됨",
|
||||
"toast.workspace.disabled.description": "사이드바에 메인 작업 트리만 표시됩니다",
|
||||
"toast.permissions.autoaccept.on.title": "편집 자동 수락 중",
|
||||
"toast.permissions.autoaccept.on.description": "편집 및 쓰기 권한이 자동으로 승인됩니다",
|
||||
"toast.permissions.autoaccept.off.title": "편집 자동 수락 중지됨",
|
||||
"toast.permissions.autoaccept.off.description": "편집 및 쓰기 권한 승인이 필요합니다",
|
||||
"toast.permissions.autoaccept.on.title": "권한 자동 수락 중",
|
||||
"toast.permissions.autoaccept.on.description": "권한 요청이 자동으로 승인됩니다",
|
||||
"toast.permissions.autoaccept.off.title": "권한 자동 수락 중지됨",
|
||||
"toast.permissions.autoaccept.off.description": "권한 요청에 승인이 필요합니다",
|
||||
"toast.model.none.title": "선택된 모델 없음",
|
||||
"toast.model.none.description": "이 세션을 요약하려면 공급자를 연결하세요",
|
||||
"toast.file.loadFailed.title": "파일 로드 실패",
|
||||
|
||||
@@ -74,8 +74,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
||||
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
||||
"command.permissions.autoaccept.enable": "Aksepter tillatelser automatisk",
|
||||
"command.permissions.autoaccept.disable": "Stopp automatisk akseptering av tillatelser",
|
||||
"command.workspace.toggle": "Veksle arbeidsområder",
|
||||
"command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar",
|
||||
"command.session.undo": "Angre",
|
||||
@@ -102,8 +102,10 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Andre",
|
||||
"dialog.provider.tag.recommended": "Anbefalt",
|
||||
"dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer",
|
||||
"dialog.provider.opencode.tagline": "Pålitelige, optimaliserte modeller",
|
||||
"dialog.provider.opencodeGo.tagline": "Rimelig abonnement for alle",
|
||||
"dialog.provider.anthropic.note": "Direkte tilgang til Claude-modeller, inkludert Pro og Max",
|
||||
"dialog.provider.copilot.note": "Claude-modeller for kodeassistanse",
|
||||
"dialog.provider.copilot.note": "AI-modeller for kodeassistanse via GitHub Copilot",
|
||||
"dialog.provider.openai.note": "GPT-modeller for raske, dyktige generelle AI-oppgaver",
|
||||
"dialog.provider.google.note": "Gemini-modeller for raske, strukturerte svar",
|
||||
"dialog.provider.openrouter.note": "Tilgang til alle støttede modeller fra én leverandør",
|
||||
@@ -404,10 +406,10 @@ export const dict = {
|
||||
"toast.workspace.disabled.title": "Arbeidsområder deaktivert",
|
||||
"toast.workspace.disabled.description": "Kun hoved-worktree vises i sidefeltet",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Godtar endringer automatisk",
|
||||
"toast.permissions.autoaccept.on.description": "Redigerings- og skrivetillatelser vil bli godkjent automatisk",
|
||||
"toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk",
|
||||
"toast.permissions.autoaccept.off.description": "Redigerings- og skrivetillatelser vil kreve godkjenning",
|
||||
"toast.permissions.autoaccept.on.title": "Aksepterer tillatelser automatisk",
|
||||
"toast.permissions.autoaccept.on.description": "Forespørsler om tillatelse vil bli godkjent automatisk",
|
||||
"toast.permissions.autoaccept.off.title": "Stoppet automatisk akseptering av tillatelser",
|
||||
"toast.permissions.autoaccept.off.description": "Forespørsler om tillatelse vil kreve godkjenning",
|
||||
|
||||
"toast.model.none.title": "Ingen modell valgt",
|
||||
"toast.model.none.description": "Koble til en leverandør for å oppsummere denne sesjonen",
|
||||
|
||||
@@ -65,8 +65,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
|
||||
"command.prompt.mode.shell": "Terminal",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
|
||||
"command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji",
|
||||
"command.permissions.autoaccept.enable": "Automatycznie akceptuj uprawnienia",
|
||||
"command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie uprawnień",
|
||||
"command.workspace.toggle": "Przełącz przestrzenie robocze",
|
||||
"command.workspace.toggle.description": "Włącz lub wyłącz wiele przestrzeni roboczych na pasku bocznym",
|
||||
"command.session.undo": "Cofnij",
|
||||
@@ -91,8 +91,10 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Inne",
|
||||
"dialog.provider.tag.recommended": "Zalecane",
|
||||
"dialog.provider.opencode.note": "Wyselekcjonowane modele, w tym Claude, GPT, Gemini i inne",
|
||||
"dialog.provider.opencode.tagline": "Niezawodne, zoptymalizowane modele",
|
||||
"dialog.provider.opencodeGo.tagline": "Tania subskrypcja dla każdego",
|
||||
"dialog.provider.anthropic.note": "Bezpośredni dostęp do modeli Claude, w tym Pro i Max",
|
||||
"dialog.provider.copilot.note": "Modele Claude do pomocy w kodowaniu",
|
||||
"dialog.provider.copilot.note": "Modele AI do pomocy w kodowaniu przez GitHub Copilot",
|
||||
"dialog.provider.openai.note": "Modele GPT do szybkich i wszechstronnych zadań AI",
|
||||
"dialog.provider.google.note": "Modele Gemini do szybkich i ustrukturyzowanych odpowiedzi",
|
||||
"dialog.provider.openrouter.note": "Dostęp do wszystkich obsługiwanych modeli od jednego dostawcy",
|
||||
@@ -365,10 +367,10 @@ export const dict = {
|
||||
"toast.workspace.enabled.description": "Kilka worktree jest teraz wyświetlanych na pasku bocznym",
|
||||
"toast.workspace.disabled.title": "Przestrzenie robocze wyłączone",
|
||||
"toast.workspace.disabled.description": "Tylko główny worktree jest wyświetlany na pasku bocznym",
|
||||
"toast.permissions.autoaccept.on.title": "Automatyczne akceptowanie edycji",
|
||||
"toast.permissions.autoaccept.on.description": "Uprawnienia do edycji i zapisu będą automatycznie zatwierdzane",
|
||||
"toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie edycji",
|
||||
"toast.permissions.autoaccept.off.description": "Uprawnienia do edycji i zapisu będą wymagały zatwierdzenia",
|
||||
"toast.permissions.autoaccept.on.title": "Automatyczne akceptowanie uprawnień",
|
||||
"toast.permissions.autoaccept.on.description": "Żądania uprawnień będą automatycznie zatwierdzane",
|
||||
"toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie uprawnień",
|
||||
"toast.permissions.autoaccept.off.description": "Żądania uprawnień będą wymagały zatwierdzenia",
|
||||
"toast.model.none.title": "Nie wybrano modelu",
|
||||
"toast.model.none.description": "Połącz dostawcę, aby podsumować tę sesję",
|
||||
"toast.file.loadFailed.title": "Nie udało się załadować pliku",
|
||||
|
||||
@@ -71,8 +71,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
||||
"command.prompt.mode.shell": "Оболочка",
|
||||
"command.prompt.mode.normal": "Промпт",
|
||||
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
||||
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
||||
"command.permissions.autoaccept.enable": "Автоматически принимать разрешения",
|
||||
"command.permissions.autoaccept.disable": "Остановить автоматическое принятие разрешений",
|
||||
"command.workspace.toggle": "Переключить рабочие пространства",
|
||||
"command.workspace.toggle.description": "Включить или отключить несколько рабочих пространств в боковой панели",
|
||||
"command.session.undo": "Отменить",
|
||||
@@ -99,8 +99,10 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Другие",
|
||||
"dialog.provider.tag.recommended": "Рекомендуемые",
|
||||
"dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие",
|
||||
"dialog.provider.opencode.tagline": "Надежные оптимизированные модели",
|
||||
"dialog.provider.opencodeGo.tagline": "Доступная подписка для всех",
|
||||
"dialog.provider.anthropic.note": "Прямой доступ к моделям Claude, включая Pro и Max",
|
||||
"dialog.provider.copilot.note": "Модели Claude для помощи в кодировании",
|
||||
"dialog.provider.copilot.note": "ИИ-модели для помощи в кодировании через GitHub Copilot",
|
||||
"dialog.provider.openai.note": "Модели GPT для быстрых и мощных задач общего ИИ",
|
||||
"dialog.provider.google.note": "Модели Gemini для быстрых и структурированных ответов",
|
||||
"dialog.provider.openrouter.note": "Доступ ко всем поддерживаемым моделям через одного провайдера",
|
||||
@@ -398,10 +400,10 @@ export const dict = {
|
||||
"toast.theme.title": "Тема переключена",
|
||||
"toast.scheme.title": "Цветовая схема",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Авто-принятие изменений",
|
||||
"toast.permissions.autoaccept.on.description": "Разрешения на редактирование и запись будут автоматически одобрены",
|
||||
"toast.permissions.autoaccept.off.title": "Авто-принятие остановлено",
|
||||
"toast.permissions.autoaccept.off.description": "Редактирование и запись потребуют подтверждения",
|
||||
"toast.permissions.autoaccept.on.title": "Разрешения принимаются автоматически",
|
||||
"toast.permissions.autoaccept.on.description": "Запросы на разрешения будут одобряться автоматически",
|
||||
"toast.permissions.autoaccept.off.title": "Автоматическое принятие разрешений остановлено",
|
||||
"toast.permissions.autoaccept.off.description": "Запросы на разрешения будут требовать одобрения",
|
||||
|
||||
"toast.workspace.enabled.title": "Рабочие пространства включены",
|
||||
"toast.workspace.enabled.description": "В боковой панели теперь отображаются несколько рабочих деревьев",
|
||||
|
||||
@@ -71,8 +71,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
||||
"command.prompt.mode.shell": "เชลล์",
|
||||
"command.prompt.mode.normal": "พรอมต์",
|
||||
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"command.permissions.autoaccept.enable": "ยอมรับสิทธิ์โดยอัตโนมัติ",
|
||||
"command.permissions.autoaccept.disable": "หยุดยอมรับสิทธิ์โดยอัตโนมัติ",
|
||||
"command.workspace.toggle": "สลับพื้นที่ทำงาน",
|
||||
"command.workspace.toggle.description": "เปิดหรือปิดใช้งานพื้นที่ทำงานหลายรายการในแถบด้านข้าง",
|
||||
"command.session.undo": "ยกเลิก",
|
||||
@@ -99,8 +99,10 @@ export const dict = {
|
||||
"dialog.provider.group.other": "อื่น ๆ",
|
||||
"dialog.provider.tag.recommended": "แนะนำ",
|
||||
"dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ",
|
||||
"dialog.provider.opencode.tagline": "โมเดลที่เชื่อถือได้และปรับให้เหมาะสม",
|
||||
"dialog.provider.opencodeGo.tagline": "การสมัครสมาชิกราคาประหยัดสำหรับทุกคน",
|
||||
"dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max",
|
||||
"dialog.provider.copilot.note": "โมเดล Claude สำหรับการช่วยเหลือในการเขียนโค้ด",
|
||||
"dialog.provider.copilot.note": "โมเดล AI สำหรับการช่วยเหลือในการเขียนโค้ดผ่าน GitHub Copilot",
|
||||
"dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ",
|
||||
"dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง",
|
||||
"dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว",
|
||||
@@ -401,10 +403,10 @@ export const dict = {
|
||||
"toast.workspace.disabled.title": "ปิดใช้งานพื้นที่ทำงานแล้ว",
|
||||
"toast.workspace.disabled.description": "จะแสดงเฉพาะ worktree หลักในแถบด้านข้าง",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "กำลังยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"toast.permissions.autoaccept.on.description": "สิทธิ์การแก้ไขและจะได้รับเขียนการอนุมัติโดยอัตโนมัติ",
|
||||
"toast.permissions.autoaccept.off.title": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"toast.permissions.autoaccept.off.description": "สิทธิ์การแก้ไขและเขียนจะต้องได้รับการอนุมัติ",
|
||||
"toast.permissions.autoaccept.on.title": "กำลังยอมรับสิทธิ์โดยอัตโนมัติ",
|
||||
"toast.permissions.autoaccept.on.description": "คำขอสิทธิ์จะได้รับการอนุมัติโดยอัตโนมัติ",
|
||||
"toast.permissions.autoaccept.off.title": "หยุดยอมรับสิทธิ์โดยอัตโนมัติแล้ว",
|
||||
"toast.permissions.autoaccept.off.description": "คำขอสิทธิ์จะต้องได้รับการอนุมัติ",
|
||||
|
||||
"toast.model.none.title": "ไม่ได้เลือกโมเดล",
|
||||
"toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้",
|
||||
|
||||
@@ -96,8 +96,8 @@ export const dict = {
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
|
||||
"command.permissions.autoaccept.enable": "自动接受编辑",
|
||||
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
||||
"command.permissions.autoaccept.enable": "自动接受权限",
|
||||
"command.permissions.autoaccept.disable": "停止自动接受权限",
|
||||
|
||||
"command.workspace.toggle": "切换工作区",
|
||||
"command.workspace.toggle.description": "在侧边栏启用或禁用多个工作区",
|
||||
@@ -126,6 +126,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "其他",
|
||||
"dialog.provider.tag.recommended": "推荐",
|
||||
"dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接",
|
||||
"dialog.provider.opencode.tagline": "可靠的优化模型",
|
||||
"dialog.provider.opencodeGo.tagline": "适合所有人的低成本订阅",
|
||||
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
|
||||
"dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接",
|
||||
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
|
||||
@@ -413,10 +415,10 @@ export const dict = {
|
||||
"toast.workspace.enabled.description": "侧边栏现在显示多个工作树",
|
||||
"toast.workspace.disabled.title": "工作区已禁用",
|
||||
"toast.workspace.disabled.description": "侧边栏只显示主工作树",
|
||||
"toast.permissions.autoaccept.on.title": "自动接受编辑",
|
||||
"toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批",
|
||||
"toast.permissions.autoaccept.off.title": "已停止自动接受编辑",
|
||||
"toast.permissions.autoaccept.off.description": "编辑和写入权限将需要手动批准",
|
||||
"toast.permissions.autoaccept.on.title": "正在自动接受权限",
|
||||
"toast.permissions.autoaccept.on.description": "权限请求将被自动批准",
|
||||
"toast.permissions.autoaccept.off.title": "已停止自动接受权限",
|
||||
"toast.permissions.autoaccept.off.description": "权限请求将需要批准",
|
||||
"toast.model.none.title": "未选择模型",
|
||||
"toast.model.none.description": "请先连接提供商以总结此会话",
|
||||
"toast.file.loadFailed.title": "加载文件失败",
|
||||
|
||||
@@ -75,8 +75,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "自動接受編輯",
|
||||
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
||||
"command.permissions.autoaccept.enable": "自動接受權限",
|
||||
"command.permissions.autoaccept.disable": "停止自動接受權限",
|
||||
"command.workspace.toggle": "切換工作區",
|
||||
"command.workspace.toggle.description": "在側邊欄啟用或停用多個工作區",
|
||||
"command.session.undo": "復原",
|
||||
@@ -103,6 +103,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "其他",
|
||||
"dialog.provider.tag.recommended": "推薦",
|
||||
"dialog.provider.opencode.note": "精選模型,包含 Claude、GPT、Gemini 等等",
|
||||
"dialog.provider.opencode.tagline": "可靠的優化模型",
|
||||
"dialog.provider.opencodeGo.tagline": "適合所有人的低成本訂閱",
|
||||
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 金鑰連線",
|
||||
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 金鑰連線",
|
||||
"dialog.provider.copilot.note": "使用 Copilot 或 API 金鑰連線",
|
||||
@@ -400,10 +402,10 @@ export const dict = {
|
||||
"toast.workspace.disabled.title": "工作區已停用",
|
||||
"toast.workspace.disabled.description": "側邊欄只顯示主工作樹",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "自動接受編輯",
|
||||
"toast.permissions.autoaccept.on.description": "編輯和寫入權限將自動獲准",
|
||||
"toast.permissions.autoaccept.off.title": "已停止自動接受編輯",
|
||||
"toast.permissions.autoaccept.off.description": "編輯和寫入權限將需要手動批准",
|
||||
"toast.permissions.autoaccept.on.title": "正在自動接受權限",
|
||||
"toast.permissions.autoaccept.on.description": "權限請求將被自動批准",
|
||||
"toast.permissions.autoaccept.off.title": "已停止自動接受權限",
|
||||
"toast.permissions.autoaccept.off.description": "權限請求將需要批准",
|
||||
|
||||
"toast.model.none.title": "未選擇模型",
|
||||
"toast.model.none.description": "請先連線提供者以總結此工作階段",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { createEffect, createMemo, Show, type ParentProps } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||
import { SDKProvider } from "@/context/sdk"
|
||||
import { SyncProvider, useSync } from "@/context/sync"
|
||||
import { LocalProvider } from "@/context/local"
|
||||
|
||||
import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -15,19 +14,11 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
|
||||
return (
|
||||
<DataProvider
|
||||
data={sync.data}
|
||||
directory={props.directory}
|
||||
onPermissionRespond={(input: {
|
||||
sessionID: string
|
||||
permissionID: string
|
||||
response: "once" | "always" | "reject"
|
||||
}) => sdk.client.permission.respond(input)}
|
||||
onQuestionReply={(input: { requestID: string; answers: QuestionAnswer[] }) => sdk.client.question.reply(input)}
|
||||
onQuestionReject={(input: { requestID: string }) => sdk.client.question.reject(input)}
|
||||
onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/session/${sessionID}`)}
|
||||
onSessionHref={(sessionID: string) => `/${params.dir}/session/${sessionID}`}
|
||||
>
|
||||
|
||||
@@ -43,6 +43,7 @@ import { retry } from "@opencode-ai/util/retry"
|
||||
import { playSound, soundSrc } from "@/utils/sound"
|
||||
import { createAim } from "@/utils/aim"
|
||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
@@ -61,11 +62,17 @@ import {
|
||||
displayName,
|
||||
errorMessage,
|
||||
getDraggableId,
|
||||
latestRootSession,
|
||||
sortedRootSessions,
|
||||
syncWorkspaceOrder,
|
||||
workspaceKey,
|
||||
} from "./layout/helpers"
|
||||
import { collectOpenProjectDeepLinks, deepLinkEvent, drainPendingDeepLinks } from "./layout/deep-links"
|
||||
import {
|
||||
collectNewSessionDeepLinks,
|
||||
collectOpenProjectDeepLinks,
|
||||
deepLinkEvent,
|
||||
drainPendingDeepLinks,
|
||||
} from "./layout/deep-links"
|
||||
import { createInlineEditorController } from "./layout/inline-editor"
|
||||
import {
|
||||
LocalWorkspace,
|
||||
@@ -1093,14 +1100,51 @@ export default function Layout(props: ParentProps) {
|
||||
return meta?.worktree ?? directory
|
||||
}
|
||||
|
||||
function navigateToProject(directory: string | undefined) {
|
||||
async function navigateToProject(directory: string | undefined) {
|
||||
if (!directory) return
|
||||
const root = projectRoot(directory)
|
||||
server.projects.touch(root)
|
||||
const project = layout.projects.list().find((item) => item.worktree === root)
|
||||
const dirs = Array.from(new Set([root, ...(store.workspaceOrder[root] ?? []), ...(project?.sandboxes ?? [])]))
|
||||
const openSession = async (target: { directory: string; id: string }) => {
|
||||
const resolved = await globalSDK.client.session
|
||||
.get({ sessionID: target.id })
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
const next = resolved?.directory ? resolved : target
|
||||
setStore("lastProjectSession", root, { directory: next.directory, id: next.id, at: Date.now() })
|
||||
navigateWithSidebarReset(`/${base64Encode(next.directory)}/session/${next.id}`)
|
||||
}
|
||||
|
||||
const projectSession = store.lastProjectSession[root]
|
||||
if (projectSession?.id) {
|
||||
navigateWithSidebarReset(`/${base64Encode(projectSession.directory)}/session/${projectSession.id}`)
|
||||
await openSession(projectSession)
|
||||
return
|
||||
}
|
||||
|
||||
const latest = latestRootSession(
|
||||
dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]),
|
||||
Date.now(),
|
||||
)
|
||||
if (latest) {
|
||||
await openSession(latest)
|
||||
return
|
||||
}
|
||||
|
||||
const fetched = latestRootSession(
|
||||
await Promise.all(
|
||||
dirs.map(async (item) => ({
|
||||
path: { directory: item },
|
||||
session: await globalSDK.client.session
|
||||
.list({ directory: item })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => []),
|
||||
})),
|
||||
),
|
||||
Date.now(),
|
||||
)
|
||||
if (fetched) {
|
||||
await openSession(fetched)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1119,9 +1163,20 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const handleDeepLinks = (urls: string[]) => {
|
||||
if (!server.isLocal()) return
|
||||
|
||||
for (const directory of collectOpenProjectDeepLinks(urls)) {
|
||||
openProject(directory)
|
||||
}
|
||||
|
||||
for (const link of collectNewSessionDeepLinks(urls)) {
|
||||
openProject(link.directory, false)
|
||||
const slug = base64Encode(link.directory)
|
||||
if (link.prompt) {
|
||||
setSessionHandoff(slug, { prompt: link.prompt })
|
||||
}
|
||||
const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session`
|
||||
navigateWithSidebarReset(href)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
export const deepLinkEvent = "opencode:deep-link"
|
||||
|
||||
export const parseDeepLink = (input: string) => {
|
||||
const parseUrl = (input: string) => {
|
||||
if (!input.startsWith("opencode://")) return
|
||||
if (typeof URL.canParse === "function" && !URL.canParse(input)) return
|
||||
const url = (() => {
|
||||
try {
|
||||
return new URL(input)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
try {
|
||||
return new URL(input)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export const parseDeepLink = (input: string) => {
|
||||
const url = parseUrl(input)
|
||||
if (!url) return
|
||||
if (url.hostname !== "open-project") return
|
||||
const directory = url.searchParams.get("directory")
|
||||
@@ -17,9 +19,23 @@ export const parseDeepLink = (input: string) => {
|
||||
return directory
|
||||
}
|
||||
|
||||
export const parseNewSessionDeepLink = (input: string) => {
|
||||
const url = parseUrl(input)
|
||||
if (!url) return
|
||||
if (url.hostname !== "new-session") return
|
||||
const directory = url.searchParams.get("directory")
|
||||
if (!directory) return
|
||||
const prompt = url.searchParams.get("prompt") || undefined
|
||||
if (!prompt) return { directory }
|
||||
return { directory, prompt }
|
||||
}
|
||||
|
||||
export const collectOpenProjectDeepLinks = (urls: string[]) =>
|
||||
urls.map(parseDeepLink).filter((directory): directory is string => !!directory)
|
||||
|
||||
export const collectNewSessionDeepLinks = (urls: string[]) =>
|
||||
urls.map(parseNewSessionDeepLink).filter((link): link is { directory: string; prompt?: string } => !!link)
|
||||
|
||||
type OpenCodeWindow = Window & {
|
||||
__OPENCODE__?: {
|
||||
deepLinks?: string[]
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links"
|
||||
import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
collectNewSessionDeepLinks,
|
||||
collectOpenProjectDeepLinks,
|
||||
drainPendingDeepLinks,
|
||||
parseDeepLink,
|
||||
parseNewSessionDeepLink,
|
||||
} from "./deep-links"
|
||||
import {
|
||||
displayName,
|
||||
errorMessage,
|
||||
getDraggableId,
|
||||
hasProjectPermissions,
|
||||
latestRootSession,
|
||||
syncWorkspaceOrder,
|
||||
workspaceKey,
|
||||
} from "./helpers"
|
||||
|
||||
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
|
||||
({
|
||||
title: "",
|
||||
version: "v2",
|
||||
parentID: undefined,
|
||||
messageCount: 0,
|
||||
permissions: { session: {}, share: {} },
|
||||
time: { created: 0, updated: 0, archived: undefined },
|
||||
...input,
|
||||
}) as Session
|
||||
|
||||
describe("layout deep links", () => {
|
||||
test("parses open-project deep links", () => {
|
||||
@@ -42,6 +68,28 @@ describe("layout deep links", () => {
|
||||
expect(result).toEqual(["/a", "/c"])
|
||||
})
|
||||
|
||||
test("parses new-session deep links with optional prompt", () => {
|
||||
expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo")).toEqual({ directory: "/tmp/demo" })
|
||||
expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo&prompt=hello%20world")).toEqual({
|
||||
directory: "/tmp/demo",
|
||||
prompt: "hello world",
|
||||
})
|
||||
})
|
||||
|
||||
test("ignores new-session deep links without directory", () => {
|
||||
expect(parseNewSessionDeepLink("opencode://new-session")).toBeUndefined()
|
||||
expect(parseNewSessionDeepLink("opencode://new-session?directory=")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("collects only valid new-session deep links", () => {
|
||||
const result = collectNewSessionDeepLinks([
|
||||
"opencode://new-session?directory=/a",
|
||||
"opencode://open-project?directory=/b",
|
||||
"opencode://new-session?directory=/c&prompt=ship%20it",
|
||||
])
|
||||
expect(result).toEqual([{ directory: "/a" }, { directory: "/c", prompt: "ship it" }])
|
||||
})
|
||||
|
||||
test("drains global deep links once", () => {
|
||||
const target = {
|
||||
__OPENCODE__: {
|
||||
@@ -73,6 +121,84 @@ describe("layout workspace helpers", () => {
|
||||
expect(result).toEqual(["/root", "/c", "/b"])
|
||||
})
|
||||
|
||||
test("finds the latest root session across workspaces", () => {
|
||||
const result = latestRootSession(
|
||||
[
|
||||
{
|
||||
path: { directory: "/root" },
|
||||
session: [session({ id: "root", directory: "/root", time: { created: 1, updated: 1, archived: undefined } })],
|
||||
},
|
||||
{
|
||||
path: { directory: "/workspace" },
|
||||
session: [
|
||||
session({
|
||||
id: "workspace",
|
||||
directory: "/workspace",
|
||||
time: { created: 2, updated: 2, archived: undefined },
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
120_000,
|
||||
)
|
||||
|
||||
expect(result?.id).toBe("workspace")
|
||||
})
|
||||
|
||||
test("detects project permissions with a filter", () => {
|
||||
const result = hasProjectPermissions(
|
||||
{
|
||||
root: [{ id: "perm-root" }, { id: "perm-hidden" }],
|
||||
child: [{ id: "perm-child" }],
|
||||
},
|
||||
(item) => item.id === "perm-child",
|
||||
)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("ignores project permissions filtered out", () => {
|
||||
const result = hasProjectPermissions(
|
||||
{
|
||||
root: [{ id: "perm-root" }],
|
||||
},
|
||||
() => false,
|
||||
)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
test("ignores archived and child sessions when finding latest root session", () => {
|
||||
const result = latestRootSession(
|
||||
[
|
||||
{
|
||||
path: { directory: "/workspace" },
|
||||
session: [
|
||||
session({
|
||||
id: "archived",
|
||||
directory: "/workspace",
|
||||
time: { created: 10, updated: 10, archived: 10 },
|
||||
}),
|
||||
session({
|
||||
id: "child",
|
||||
directory: "/workspace",
|
||||
parentID: "parent",
|
||||
time: { created: 20, updated: 20, archived: undefined },
|
||||
}),
|
||||
session({
|
||||
id: "root",
|
||||
directory: "/workspace",
|
||||
time: { created: 30, updated: 30, archived: undefined },
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
120_000,
|
||||
)
|
||||
|
||||
expect(result?.id).toBe("root")
|
||||
})
|
||||
|
||||
test("extracts draggable id safely", () => {
|
||||
expect(getDraggableId({ draggable: { id: "x" } })).toBe("x")
|
||||
expect(getDraggableId({ draggable: { id: 42 } })).toBeUndefined()
|
||||
|
||||
@@ -28,6 +28,18 @@ export const isRootVisibleSession = (session: Session, directory: string) =>
|
||||
export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) =>
|
||||
store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now))
|
||||
|
||||
export const latestRootSession = (stores: { session: Session[]; path: { directory: string } }[], now: number) =>
|
||||
stores
|
||||
.flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory)))
|
||||
.sort(sortSessions(now))[0]
|
||||
|
||||
export function hasProjectPermissions<T>(
|
||||
request: Record<string, T[] | undefined>,
|
||||
include: (item: T) => boolean = () => true,
|
||||
) {
|
||||
return Object.values(request).some((list) => list?.some(include))
|
||||
}
|
||||
|
||||
export const childMapByParent = (sessions: Session[]) => {
|
||||
const map = new Map<string, string[]>()
|
||||
for (const session of sessions) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout, type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
@@ -16,16 +17,27 @@ import { getFilename } from "@opencode-ai/util/path"
|
||||
import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js"
|
||||
import { agentColor } from "@/utils/agent"
|
||||
import { hasProjectPermissions } from "./helpers"
|
||||
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
|
||||
|
||||
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
|
||||
const unseenCount = createMemo(() =>
|
||||
dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
|
||||
)
|
||||
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
|
||||
const hasPermissions = createMemo(() =>
|
||||
dirs().some((directory) => {
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory))
|
||||
}),
|
||||
)
|
||||
const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
return (
|
||||
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
|
||||
@@ -37,15 +49,16 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
|
||||
}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class="size-full rounded"
|
||||
classList={{ "badge-mask": unseenCount() > 0 && props.notify }}
|
||||
classList={{ "badge-mask": notify() }}
|
||||
/>
|
||||
</div>
|
||||
<Show when={unseenCount() > 0 && props.notify}>
|
||||
<Show when={notify()}>
|
||||
<div
|
||||
classList={{
|
||||
"absolute top-px right-px size-1.5 rounded-full z-10": true,
|
||||
"bg-icon-critical-base": hasError(),
|
||||
"bg-text-interactive-base": !hasError(),
|
||||
"bg-surface-warning-strong": hasPermissions(),
|
||||
"bg-icon-critical-base": !hasPermissions() && hasError(),
|
||||
"bg-text-interactive-base": !hasPermissions() && !hasError(),
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
@@ -186,19 +199,15 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
const layout = useLayout()
|
||||
const language = useLanguage()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const globalSync = useGlobalSync()
|
||||
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
|
||||
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
|
||||
const [sessionStore] = globalSync.child(props.session.directory)
|
||||
const hasPermissions = createMemo(() => {
|
||||
const permissions = sessionStore.permission?.[props.session.id] ?? []
|
||||
if (permissions.length > 0) return true
|
||||
|
||||
for (const id of props.children.get(props.session.id) ?? []) {
|
||||
const childPermissions = sessionStore.permission?.[id] ?? []
|
||||
if (childPermissions.length > 0) return true
|
||||
}
|
||||
return false
|
||||
return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
|
||||
return !permission.autoResponds(item, props.session.directory)
|
||||
})
|
||||
})
|
||||
const isWorking = createMemo(() => {
|
||||
if (hasPermissions()) return false
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createEffect, createMemo, createSignal, For, Show, type Accessor, type JSX } from "solid-js"
|
||||
import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { ContextMenu } from "@opencode-ai/ui/context-menu"
|
||||
@@ -7,7 +8,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||
import { type LocalProject } from "@/context/layout"
|
||||
import { useLayout, type LocalProject } from "@/context/layout"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNotification } from "@/context/notification"
|
||||
@@ -60,6 +61,7 @@ const ProjectTile = (props: {
|
||||
selected: Accessor<boolean>
|
||||
active: Accessor<boolean>
|
||||
overlay: Accessor<boolean>
|
||||
suppressHover: Accessor<boolean>
|
||||
dirs: Accessor<string[]>
|
||||
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
|
||||
onProjectMouseLeave: (worktree: string) => void
|
||||
@@ -71,9 +73,11 @@ const ProjectTile = (props: {
|
||||
closeProject: (directory: string) => void
|
||||
setMenu: (value: boolean) => void
|
||||
setOpen: (value: boolean) => void
|
||||
setSuppressHover: (value: boolean) => void
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}): JSX.Element => {
|
||||
const notification = useNotification()
|
||||
const layout = useLayout()
|
||||
const unseenCount = createMemo(() =>
|
||||
props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
|
||||
)
|
||||
@@ -107,17 +111,28 @@ const ProjectTile = (props: {
|
||||
}}
|
||||
onMouseEnter={(event: MouseEvent) => {
|
||||
if (!props.overlay()) return
|
||||
if (props.suppressHover()) return
|
||||
props.onProjectMouseEnter(props.project.worktree, event)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (props.suppressHover()) props.setSuppressHover(false)
|
||||
if (!props.overlay()) return
|
||||
props.onProjectMouseLeave(props.project.worktree)
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (!props.overlay()) return
|
||||
if (props.suppressHover()) return
|
||||
props.onProjectFocus(props.project.worktree)
|
||||
}}
|
||||
onClick={() => props.navigateToProject(props.project.worktree)}
|
||||
onClick={() => {
|
||||
if (props.selected()) {
|
||||
props.setSuppressHover(true)
|
||||
layout.sidebar.toggle()
|
||||
return
|
||||
}
|
||||
props.setSuppressHover(false)
|
||||
props.navigateToProject(props.project.worktree)
|
||||
}}
|
||||
onBlur={() => props.setOpen(false)}
|
||||
>
|
||||
<ProjectIcon project={props.project} notify />
|
||||
@@ -278,16 +293,19 @@ export const SortableProject = (props: {
|
||||
const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
|
||||
const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
|
||||
const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const [menu, setMenu] = createSignal(false)
|
||||
const [state, setState] = createStore({
|
||||
open: false,
|
||||
menu: false,
|
||||
suppressHover: false,
|
||||
})
|
||||
|
||||
const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened())
|
||||
const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened())
|
||||
const active = createMemo(() =>
|
||||
projectTileActive({
|
||||
menu: menu(),
|
||||
menu: state.menu,
|
||||
preview: preview(),
|
||||
open: open(),
|
||||
open: state.open,
|
||||
overlay: overlay(),
|
||||
hoverProject: props.ctx.hoverProject(),
|
||||
worktree: props.project.worktree,
|
||||
@@ -296,8 +314,14 @@ export const SortableProject = (props: {
|
||||
|
||||
createEffect(() => {
|
||||
if (preview()) return
|
||||
if (!open()) return
|
||||
setOpen(false)
|
||||
if (!state.open) return
|
||||
setState("open", false)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!selected()) return
|
||||
if (!state.open) return
|
||||
setState("open", false)
|
||||
})
|
||||
|
||||
const label = (directory: string) => {
|
||||
@@ -328,6 +352,7 @@ export const SortableProject = (props: {
|
||||
selected={selected}
|
||||
active={active}
|
||||
overlay={overlay}
|
||||
suppressHover={() => state.suppressHover}
|
||||
dirs={dirs}
|
||||
onProjectMouseEnter={props.ctx.onProjectMouseEnter}
|
||||
onProjectMouseLeave={props.ctx.onProjectMouseLeave}
|
||||
@@ -337,8 +362,9 @@ export const SortableProject = (props: {
|
||||
toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces}
|
||||
workspacesEnabled={props.ctx.workspacesEnabled}
|
||||
closeProject={props.ctx.closeProject}
|
||||
setMenu={setMenu}
|
||||
setOpen={setOpen}
|
||||
setMenu={(value) => setState("menu", value)}
|
||||
setOpen={(value) => setState("open", value)}
|
||||
setSuppressHover={(value) => setState("suppressHover", value)}
|
||||
language={language}
|
||||
/>
|
||||
)
|
||||
@@ -346,17 +372,18 @@ export const SortableProject = (props: {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<Show when={preview()} fallback={tile()}>
|
||||
<Show when={preview() && !selected()} fallback={tile()}>
|
||||
<HoverCard
|
||||
open={open() && !menu()}
|
||||
open={!state.suppressHover && state.open && !state.menu}
|
||||
openDelay={0}
|
||||
closeDelay={0}
|
||||
placement="right-start"
|
||||
gutter={6}
|
||||
trigger={tile()}
|
||||
onOpenChange={(value) => {
|
||||
if (menu()) return
|
||||
setOpen(value)
|
||||
if (state.menu) return
|
||||
if (value && state.suppressHover) return
|
||||
setState("open", value)
|
||||
if (value) props.ctx.setHoverSession(undefined)
|
||||
}}
|
||||
>
|
||||
@@ -371,7 +398,7 @@ export const SortableProject = (props: {
|
||||
projectChildren={projectChildren}
|
||||
workspaceSessions={workspaceSessions}
|
||||
workspaceChildren={workspaceChildren}
|
||||
setOpen={setOpen}
|
||||
setOpen={(value) => setState("open", value)}
|
||||
ctx={props.ctx}
|
||||
language={language}
|
||||
/>
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { checksum, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { base64Encode, checksum } from "@opencode-ai/util/encode"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, Match, on, onCleanup, onMount, Show, Switch, untrack } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { SessionHeader, NewSessionView } from "@/components/session"
|
||||
import { same } from "@/utils/same"
|
||||
import { type FileSelection, type SelectedLineRange, selectionFromLines, useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
||||
import { createOpenReviewFile } from "@/pages/session/helpers"
|
||||
import { createScrollSpy } from "@/pages/session/scroll-spy"
|
||||
import { SessionReviewTab, type DiffStyle, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
|
||||
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||
import { createScrollSpy } from "@/pages/session/scroll-spy"
|
||||
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
|
||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||
import { same } from "@/utils/same"
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
@@ -44,6 +43,19 @@ export default function Page() {
|
||||
const sdk = useSDK()
|
||||
const prompt = usePrompt()
|
||||
const comments = useComments()
|
||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||
|
||||
createEffect(() => {
|
||||
if (!untrack(() => prompt.ready())) return
|
||||
prompt.ready()
|
||||
untrack(() => {
|
||||
if (params.id || !prompt.ready()) return
|
||||
const text = searchParams.prompt
|
||||
if (!text) return
|
||||
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
|
||||
setSearchParams({ ...searchParams, prompt: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
const [ui, setUi] = createStore({
|
||||
pendingMessage: undefined as string | undefined,
|
||||
@@ -107,7 +119,7 @@ export default function Page() {
|
||||
if (desktopReviewOpen()) return `${layout.session.width()}px`
|
||||
return `calc(100% - ${layout.fileTree.width()}px)`
|
||||
})
|
||||
const centered = createMemo(() => isDesktop() && !desktopSidePanelOpen())
|
||||
const centered = createMemo(() => isDesktop() && !desktopReviewOpen())
|
||||
|
||||
function normalizeTab(tab: string) {
|
||||
if (!tab.startsWith("file://")) return tab
|
||||
@@ -254,12 +266,13 @@ export default function Page() {
|
||||
const msgs = visibleUserMessages()
|
||||
if (msgs.length === 0) return
|
||||
|
||||
const current = activeMessage()
|
||||
const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1
|
||||
const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset
|
||||
if (targetIndex < 0 || targetIndex >= msgs.length) return
|
||||
const current = store.messageId
|
||||
const base = current ? msgs.findIndex((m) => m.id === current) : msgs.length
|
||||
const currentIndex = base === -1 ? msgs.length : base
|
||||
const targetIndex = currentIndex + offset
|
||||
if (targetIndex < 0 || targetIndex > msgs.length) return
|
||||
|
||||
if (targetIndex === msgs.length - 1) {
|
||||
if (targetIndex === msgs.length) {
|
||||
resumeScroll()
|
||||
return
|
||||
}
|
||||
@@ -378,11 +391,58 @@ export default function Page() {
|
||||
})
|
||||
}
|
||||
|
||||
const updateCommentInContext = (input: {
|
||||
id: string
|
||||
file: string
|
||||
selection: SelectedLineRange
|
||||
comment: string
|
||||
preview?: string
|
||||
}) => {
|
||||
comments.update(input.file, input.id, input.comment)
|
||||
prompt.context.updateComment(input.file, input.id, {
|
||||
comment: input.comment,
|
||||
...(input.preview ? { preview: input.preview } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
const removeCommentFromContext = (input: { id: string; file: string }) => {
|
||||
comments.remove(input.file, input.id)
|
||||
prompt.context.removeComment(input.file, input.id)
|
||||
}
|
||||
|
||||
const reviewCommentActions = createMemo(() => ({
|
||||
moreLabel: language.t("common.moreOptions"),
|
||||
editLabel: language.t("common.edit"),
|
||||
deleteLabel: language.t("common.delete"),
|
||||
saveLabel: language.t("common.save"),
|
||||
}))
|
||||
|
||||
const isEditableTarget = (target: EventTarget | null | undefined) => {
|
||||
if (!(target instanceof HTMLElement)) return false
|
||||
return /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(target.tagName) || target.isContentEditable
|
||||
}
|
||||
|
||||
const deepActiveElement = () => {
|
||||
let current: Element | null = document.activeElement
|
||||
while (current instanceof HTMLElement && current.shadowRoot?.activeElement) {
|
||||
current = current.shadowRoot.activeElement
|
||||
}
|
||||
return current instanceof HTMLElement ? current : undefined
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const activeElement = document.activeElement as HTMLElement | undefined
|
||||
const path = event.composedPath()
|
||||
const target = path.find((item): item is HTMLElement => item instanceof HTMLElement)
|
||||
const activeElement = deepActiveElement()
|
||||
|
||||
const protectedTarget = path.some(
|
||||
(item) => item instanceof HTMLElement && item.closest("[data-prevent-autofocus]") !== null,
|
||||
)
|
||||
if (protectedTarget || isEditableTarget(target)) return
|
||||
|
||||
if (activeElement) {
|
||||
const isProtected = activeElement.closest("[data-prevent-autofocus]")
|
||||
const isInput = /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(activeElement.tagName) || activeElement.isContentEditable
|
||||
const isInput = isEditableTarget(activeElement)
|
||||
if (isProtected || isInput) return
|
||||
}
|
||||
if (dialog.active) return
|
||||
@@ -415,7 +475,7 @@ export default function Page() {
|
||||
)
|
||||
|
||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
||||
const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened())
|
||||
const reviewTab = createMemo(() => isDesktop())
|
||||
|
||||
const fileTreeTab = () => layout.fileTree.tab()
|
||||
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
|
||||
@@ -430,7 +490,11 @@ export default function Page() {
|
||||
on(
|
||||
sessionKey,
|
||||
() => {
|
||||
setTree({ reviewScroll: undefined, pendingDiff: undefined, activeDiff: undefined })
|
||||
setTree({
|
||||
reviewScroll: undefined,
|
||||
pendingDiff: undefined,
|
||||
activeDiff: undefined,
|
||||
})
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
@@ -468,7 +532,8 @@ export default function Page() {
|
||||
}
|
||||
onSelect={(option) => option && setStore("changes", option)}
|
||||
variant="ghost"
|
||||
size="large"
|
||||
size="small"
|
||||
valueClass="text-14-medium"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -498,6 +563,9 @@ export default function Page() {
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
focusedFile={tree.activeDiff}
|
||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||
onLineCommentUpdate={updateCommentInContext}
|
||||
onLineCommentDelete={removeCommentFromContext}
|
||||
lineCommentActions={reviewCommentActions()}
|
||||
comments={comments.all()}
|
||||
focusedComment={comments.focus()}
|
||||
onFocusedCommentChange={comments.setFocus}
|
||||
@@ -519,6 +587,9 @@ export default function Page() {
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
focusedFile={tree.activeDiff}
|
||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||
onLineCommentUpdate={updateCommentInContext}
|
||||
onLineCommentDelete={removeCommentFromContext}
|
||||
lineCommentActions={reviewCommentActions()}
|
||||
comments={comments.all()}
|
||||
focusedComment={comments.focus()}
|
||||
onFocusedCommentChange={comments.setFocus}
|
||||
@@ -547,6 +618,9 @@ export default function Page() {
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
focusedFile={tree.activeDiff}
|
||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||
onLineCommentUpdate={updateCommentInContext}
|
||||
onLineCommentDelete={removeCommentFromContext}
|
||||
lineCommentActions={reviewCommentActions()}
|
||||
comments={comments.all()}
|
||||
focusedComment={comments.focus()}
|
||||
onFocusedCommentChange={comments.setFocus}
|
||||
@@ -699,33 +773,12 @@ export default function Page() {
|
||||
const active = tabs().active()
|
||||
const tab = active === "review" || (!active && hasReview()) ? "changes" : "all"
|
||||
layout.fileTree.setTab(tab)
|
||||
return
|
||||
}
|
||||
|
||||
if (fileTreeTab() !== "changes") return
|
||||
tabs().setActive("review")
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (!isDesktop()) return
|
||||
if (!layout.fileTree.opened()) return
|
||||
if (fileTreeTab() !== "all") return
|
||||
|
||||
const active = tabs().active()
|
||||
if (active && active !== "review") return
|
||||
|
||||
const first = openedTabs()[0]
|
||||
if (first) {
|
||||
tabs().setActive(first)
|
||||
return
|
||||
}
|
||||
|
||||
if (contextOpen()) tabs().setActive("context")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
|
||||
|
||||
const session = (input: { id: string; parentID?: string }) =>
|
||||
({
|
||||
id: input.id,
|
||||
parentID: input.parentID,
|
||||
}) as Session
|
||||
|
||||
const permission = (id: string, sessionID: string) =>
|
||||
({
|
||||
id,
|
||||
sessionID,
|
||||
}) as PermissionRequest
|
||||
|
||||
const question = (id: string, sessionID: string) =>
|
||||
({
|
||||
id,
|
||||
sessionID,
|
||||
questions: [],
|
||||
}) as QuestionRequest
|
||||
|
||||
describe("sessionPermissionRequest", () => {
|
||||
test("prefers the current session permission", () => {
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
const permissions = {
|
||||
root: [permission("perm-root", "root")],
|
||||
child: [permission("perm-child", "child")],
|
||||
}
|
||||
|
||||
expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-root")
|
||||
})
|
||||
|
||||
test("returns a nested child permission", () => {
|
||||
const sessions = [
|
||||
session({ id: "root" }),
|
||||
session({ id: "child", parentID: "root" }),
|
||||
session({ id: "grand", parentID: "child" }),
|
||||
session({ id: "other" }),
|
||||
]
|
||||
const permissions = {
|
||||
grand: [permission("perm-grand", "grand")],
|
||||
other: [permission("perm-other", "other")],
|
||||
}
|
||||
|
||||
expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-grand")
|
||||
})
|
||||
|
||||
test("returns undefined without a matching tree permission", () => {
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
const permissions = {
|
||||
other: [permission("perm-other", "other")],
|
||||
}
|
||||
|
||||
expect(sessionPermissionRequest(sessions, permissions, "root")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("skips filtered permissions in the current tree", () => {
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
const permissions = {
|
||||
root: [permission("perm-root", "root")],
|
||||
child: [permission("perm-child", "child")],
|
||||
}
|
||||
|
||||
expect(sessionPermissionRequest(sessions, permissions, "root", (item) => item.id !== "perm-root"))?.toMatchObject({
|
||||
id: "perm-child",
|
||||
})
|
||||
})
|
||||
|
||||
test("returns undefined when all tree permissions are filtered out", () => {
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
const permissions = {
|
||||
root: [permission("perm-root", "root")],
|
||||
child: [permission("perm-child", "child")],
|
||||
}
|
||||
|
||||
expect(sessionPermissionRequest(sessions, permissions, "root", () => false)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("sessionQuestionRequest", () => {
|
||||
test("prefers the current session question", () => {
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
const questions = {
|
||||
root: [question("q-root", "root")],
|
||||
child: [question("q-child", "child")],
|
||||
}
|
||||
|
||||
expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-root")
|
||||
})
|
||||
|
||||
test("returns a nested child question", () => {
|
||||
const sessions = [
|
||||
session({ id: "root" }),
|
||||
session({ id: "child", parentID: "root" }),
|
||||
session({ id: "grand", parentID: "child" }),
|
||||
]
|
||||
const questions = {
|
||||
grand: [question("q-grand", "grand")],
|
||||
}
|
||||
|
||||
expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand")
|
||||
})
|
||||
})
|
||||
@@ -5,16 +5,27 @@ import { useParams } from "@solidjs/router"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
|
||||
|
||||
export function createSessionComposerBlocked() {
|
||||
const params = useParams()
|
||||
const permission = usePermission()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const permissionRequest = createMemo(() =>
|
||||
sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => {
|
||||
return !permission.autoResponds(item, sdk.directory)
|
||||
}),
|
||||
)
|
||||
const questionRequest = createMemo(() => sessionQuestionRequest(sync.data.session, sync.data.question, params.id))
|
||||
|
||||
return createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return false
|
||||
return !!sync.data.permission[id]?.[0] || !!sync.data.question[id]?.[0]
|
||||
return !!permissionRequest() || !!questionRequest()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,20 +35,23 @@ export function createSessionComposerState() {
|
||||
const sync = useSync()
|
||||
const globalSync = useGlobalSync()
|
||||
const language = useLanguage()
|
||||
const permission = usePermission()
|
||||
|
||||
const questionRequest = createMemo((): QuestionRequest | undefined => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
return sync.data.question[id]?.[0]
|
||||
return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
|
||||
})
|
||||
|
||||
const permissionRequest = createMemo((): PermissionRequest | undefined => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
return sync.data.permission[id]?.[0]
|
||||
return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => {
|
||||
return !permission.autoResponds(item, sdk.directory)
|
||||
})
|
||||
})
|
||||
|
||||
const blocked = createSessionComposerBlocked()
|
||||
const blocked = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return false
|
||||
return !!permissionRequest() || !!questionRequest()
|
||||
})
|
||||
|
||||
const todos = createMemo((): Todo[] => {
|
||||
const id = params.id
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
function sessionTreeRequest<T>(
|
||||
session: Session[],
|
||||
request: Record<string, T[] | undefined>,
|
||||
sessionID?: string,
|
||||
include: (item: T) => boolean = () => true,
|
||||
) {
|
||||
if (!sessionID) return
|
||||
|
||||
const map = session.reduce((acc, item) => {
|
||||
if (!item.parentID) return acc
|
||||
const list = acc.get(item.parentID)
|
||||
if (list) list.push(item.id)
|
||||
if (!list) acc.set(item.parentID, [item.id])
|
||||
return acc
|
||||
}, new Map<string, string[]>())
|
||||
|
||||
const seen = new Set([sessionID])
|
||||
const ids = [sessionID]
|
||||
for (const id of ids) {
|
||||
const list = map.get(id)
|
||||
if (!list) continue
|
||||
for (const child of list) {
|
||||
if (seen.has(child)) continue
|
||||
seen.add(child)
|
||||
ids.push(child)
|
||||
}
|
||||
}
|
||||
|
||||
const id = ids.find((id) => request[id]?.some(include))
|
||||
if (!id) return
|
||||
return request[id]?.find(include)
|
||||
}
|
||||
|
||||
export function sessionPermissionRequest(
|
||||
session: Session[],
|
||||
request: Record<string, PermissionRequest[] | undefined>,
|
||||
sessionID?: string,
|
||||
include?: (item: PermissionRequest) => boolean,
|
||||
) {
|
||||
return sessionTreeRequest(session, request, sessionID, include)
|
||||
}
|
||||
|
||||
export function sessionQuestionRequest(
|
||||
session: Session[],
|
||||
request: Record<string, QuestionRequest[] | undefined>,
|
||||
sessionID?: string,
|
||||
include?: (item: QuestionRequest) => boolean,
|
||||
) {
|
||||
return sessionTreeRequest(session, request, sessionID, include)
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import { createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createEffect, createMemo, Match, on, onCleanup, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||
import type { FileSearchHandle } from "@opencode-ai/ui/file"
|
||||
import { useFileComponent } from "@opencode-ai/ui/context/file"
|
||||
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
||||
import { createLineCommentController } from "@opencode-ai/ui/line-comment-annotations"
|
||||
import { sampledChecksum } from "@opencode-ai/util/encode"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||
import { useComments } from "@/context/comments"
|
||||
@@ -17,11 +19,37 @@ import { useLanguage } from "@/context/language"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { getSessionHandoff } from "@/pages/session/handoff"
|
||||
|
||||
const formatCommentLabel = (range: SelectedLineRange) => {
|
||||
const start = Math.min(range.start, range.end)
|
||||
const end = Math.max(range.start, range.end)
|
||||
if (start === end) return `line ${start}`
|
||||
return `lines ${start}-${end}`
|
||||
function FileCommentMenu(props: {
|
||||
moreLabel: string
|
||||
editLabel: string
|
||||
deleteLabel: string
|
||||
onEdit: VoidFunction
|
||||
onDelete: VoidFunction
|
||||
}) {
|
||||
return (
|
||||
<div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
|
||||
<DropdownMenu gutter={4} placement="bottom-end">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
class="size-6 rounded-md"
|
||||
aria-label={props.moreLabel}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item onSelect={props.onEdit}>
|
||||
<DropdownMenu.ItemLabel>{props.editLabel}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={props.onDelete}>
|
||||
<DropdownMenu.ItemLabel>{props.deleteLabel}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FileTabContent(props: { tab: string }) {
|
||||
@@ -31,7 +59,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
const comments = useComments()
|
||||
const language = useLanguage()
|
||||
const prompt = usePrompt()
|
||||
const codeComponent = useCodeComponent()
|
||||
const fileComponent = useFileComponent()
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
@@ -41,6 +69,13 @@ export function FileTabContent(props: { tab: string }) {
|
||||
let scrollFrame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
let codeScroll: HTMLElement[] = []
|
||||
let find: FileSearchHandle | null = null
|
||||
|
||||
const search = {
|
||||
register: (handle: FileSearchHandle | null) => {
|
||||
find = handle
|
||||
},
|
||||
}
|
||||
|
||||
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||
const state = createMemo(() => {
|
||||
@@ -50,66 +85,18 @@ export function FileTabContent(props: { tab: string }) {
|
||||
})
|
||||
const contents = createMemo(() => state()?.content?.content ?? "")
|
||||
const cacheKey = createMemo(() => sampledChecksum(contents()))
|
||||
const isImage = createMemo(() => {
|
||||
const c = state()?.content
|
||||
return c?.encoding === "base64" && c?.mimeType?.startsWith("image/") && c?.mimeType !== "image/svg+xml"
|
||||
})
|
||||
const isSvg = createMemo(() => {
|
||||
const c = state()?.content
|
||||
return c?.mimeType === "image/svg+xml"
|
||||
})
|
||||
const isBinary = createMemo(() => state()?.content?.type === "binary")
|
||||
const svgContent = createMemo(() => {
|
||||
if (!isSvg()) return
|
||||
const c = state()?.content
|
||||
if (!c) return
|
||||
if (c.encoding !== "base64") return c.content
|
||||
return decode64(c.content)
|
||||
})
|
||||
|
||||
const svgDecodeFailed = createMemo(() => {
|
||||
if (!isSvg()) return false
|
||||
const c = state()?.content
|
||||
if (!c) return false
|
||||
if (c.encoding !== "base64") return false
|
||||
return svgContent() === undefined
|
||||
})
|
||||
|
||||
const svgToast = { shown: false }
|
||||
createEffect(() => {
|
||||
if (!svgDecodeFailed()) return
|
||||
if (svgToast.shown) return
|
||||
svgToast.shown = true
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.file.loadFailed.title"),
|
||||
})
|
||||
})
|
||||
const svgPreviewUrl = createMemo(() => {
|
||||
if (!isSvg()) return
|
||||
const c = state()?.content
|
||||
if (!c) return
|
||||
if (c.encoding === "base64") return `data:image/svg+xml;base64,${c.content}`
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(c.content)}`
|
||||
})
|
||||
const imageDataUrl = createMemo(() => {
|
||||
if (!isImage()) return
|
||||
const c = state()?.content
|
||||
return `data:${c?.mimeType};base64,${c?.content}`
|
||||
})
|
||||
const selectedLines = createMemo(() => {
|
||||
const selectedLines = createMemo<SelectedLineRange | null>(() => {
|
||||
const p = path()
|
||||
if (!p) return null
|
||||
if (file.ready()) return file.selectedLines(p) ?? null
|
||||
return getSessionHandoff(sessionKey())?.files[p] ?? null
|
||||
if (file.ready()) return (file.selectedLines(p) as SelectedLineRange | undefined) ?? null
|
||||
return (getSessionHandoff(sessionKey())?.files[p] as SelectedLineRange | undefined) ?? null
|
||||
})
|
||||
|
||||
const selectionPreview = (source: string, selection: FileSelection) => {
|
||||
const start = Math.max(1, Math.min(selection.startLine, selection.endLine))
|
||||
const end = Math.max(selection.startLine, selection.endLine)
|
||||
const lines = source.split("\n").slice(start - 1, end)
|
||||
if (lines.length === 0) return undefined
|
||||
return lines.slice(0, 2).join("\n")
|
||||
return previewSelectedLines(source, {
|
||||
start: selection.startLine,
|
||||
end: selection.endLine,
|
||||
})
|
||||
}
|
||||
|
||||
const addCommentToContext = (input: {
|
||||
@@ -145,7 +132,25 @@ export function FileTabContent(props: { tab: string }) {
|
||||
})
|
||||
}
|
||||
|
||||
let wrap: HTMLDivElement | undefined
|
||||
const updateCommentInContext = (input: {
|
||||
id: string
|
||||
file: string
|
||||
selection: SelectedLineRange
|
||||
comment: string
|
||||
}) => {
|
||||
comments.update(input.file, input.id, input.comment)
|
||||
const preview =
|
||||
input.file === path() ? selectionPreview(contents(), selectionFromLines(input.selection)) : undefined
|
||||
prompt.context.updateComment(input.file, input.id, {
|
||||
comment: input.comment,
|
||||
...(preview ? { preview } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
const removeCommentFromContext = (input: { id: string; file: string }) => {
|
||||
comments.remove(input.file, input.id)
|
||||
prompt.context.removeComment(input.file, input.id)
|
||||
}
|
||||
|
||||
const fileComments = createMemo(() => {
|
||||
const p = path()
|
||||
@@ -153,121 +158,105 @@ export function FileTabContent(props: { tab: string }) {
|
||||
return comments.list(p)
|
||||
})
|
||||
|
||||
const commentLayout = createMemo(() => {
|
||||
return fileComments()
|
||||
.map((comment) => `${comment.id}:${comment.selection.start}:${comment.selection.end}`)
|
||||
.join("|")
|
||||
})
|
||||
|
||||
const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection))
|
||||
|
||||
const [note, setNote] = createStore({
|
||||
openedComment: null as string | null,
|
||||
commenting: null as SelectedLineRange | null,
|
||||
draft: "",
|
||||
positions: {} as Record<string, number>,
|
||||
draftTop: undefined as number | undefined,
|
||||
selected: null as SelectedLineRange | null,
|
||||
})
|
||||
|
||||
const setCommenting = (range: SelectedLineRange | null) => {
|
||||
setNote("commenting", range)
|
||||
scheduleComments()
|
||||
if (!range) return
|
||||
setNote("draft", "")
|
||||
const syncSelected = (range: SelectedLineRange | null) => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
file.setSelectedLines(p, range ? cloneSelectedLineRange(range) : null)
|
||||
}
|
||||
|
||||
const getRoot = () => {
|
||||
const el = wrap
|
||||
if (!el) return
|
||||
const activeSelection = () => note.selected ?? selectedLines()
|
||||
|
||||
const host = el.querySelector("diffs-container")
|
||||
if (!(host instanceof HTMLElement)) return
|
||||
const commentsUi = createLineCommentController({
|
||||
comments: fileComments,
|
||||
label: language.t("ui.lineComment.submit"),
|
||||
draftKey: () => path() ?? props.tab,
|
||||
state: {
|
||||
opened: () => note.openedComment,
|
||||
setOpened: (id) => setNote("openedComment", id),
|
||||
selected: () => note.selected,
|
||||
setSelected: (range) => setNote("selected", range),
|
||||
commenting: () => note.commenting,
|
||||
setCommenting: (range) => setNote("commenting", range),
|
||||
syncSelected,
|
||||
hoverSelected: syncSelected,
|
||||
},
|
||||
getHoverSelectedRange: activeSelection,
|
||||
cancelDraftOnCommentToggle: true,
|
||||
clearSelectionOnSelectionEndNull: true,
|
||||
onSubmit: ({ comment, selection }) => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
addCommentToContext({ file: p, selection, comment, origin: "file" })
|
||||
},
|
||||
onUpdate: ({ id, comment, selection }) => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
updateCommentInContext({ id, file: p, selection, comment })
|
||||
},
|
||||
onDelete: (comment) => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
removeCommentFromContext({ id: comment.id, file: p })
|
||||
},
|
||||
editSubmitLabel: language.t("common.save"),
|
||||
renderCommentActions: (_, controls) => (
|
||||
<FileCommentMenu
|
||||
moreLabel={language.t("common.moreOptions")}
|
||||
editLabel={language.t("common.edit")}
|
||||
deleteLabel={language.t("common.delete")}
|
||||
onEdit={controls.edit}
|
||||
onDelete={controls.remove}
|
||||
/>
|
||||
),
|
||||
onDraftPopoverFocusOut: (e: FocusEvent) => {
|
||||
const current = e.currentTarget as HTMLDivElement
|
||||
const target = e.relatedTarget
|
||||
if (target instanceof Node && current.contains(target)) return
|
||||
|
||||
const root = host.shadowRoot
|
||||
if (!root) return
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
const findMarker = (root: ShadowRoot, range: SelectedLineRange) => {
|
||||
const line = Math.max(range.start, range.end)
|
||||
const node = root.querySelector(`[data-line="${line}"]`)
|
||||
if (!(node instanceof HTMLElement)) return
|
||||
return node
|
||||
}
|
||||
|
||||
const markerTop = (wrapper: HTMLElement, marker: HTMLElement) => {
|
||||
const wrapperRect = wrapper.getBoundingClientRect()
|
||||
const rect = marker.getBoundingClientRect()
|
||||
return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2)
|
||||
}
|
||||
|
||||
const updateComments = () => {
|
||||
const el = wrap
|
||||
const root = getRoot()
|
||||
if (!el || !root) {
|
||||
setNote("positions", {})
|
||||
setNote("draftTop", undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const estimateTop = (range: SelectedLineRange) => {
|
||||
const line = Math.max(range.start, range.end)
|
||||
const height = 24
|
||||
const offset = 2
|
||||
return Math.max(0, (line - 1) * height + offset)
|
||||
}
|
||||
|
||||
const large = contents().length > 500_000
|
||||
|
||||
const next: Record<string, number> = {}
|
||||
for (const comment of fileComments()) {
|
||||
const marker = findMarker(root, comment.selection)
|
||||
if (marker) next[comment.id] = markerTop(el, marker)
|
||||
else if (large) next[comment.id] = estimateTop(comment.selection)
|
||||
}
|
||||
|
||||
const removed = Object.keys(note.positions).filter((id) => next[id] === undefined)
|
||||
const changed = Object.entries(next).filter(([id, top]) => note.positions[id] !== top)
|
||||
if (removed.length > 0 || changed.length > 0) {
|
||||
setNote(
|
||||
"positions",
|
||||
produce((draft) => {
|
||||
for (const id of removed) {
|
||||
delete draft[id]
|
||||
}
|
||||
|
||||
for (const [id, top] of changed) {
|
||||
draft[id] = top
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const range = note.commenting
|
||||
if (!range) {
|
||||
setNote("draftTop", undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const marker = findMarker(root, range)
|
||||
if (marker) {
|
||||
setNote("draftTop", markerTop(el, marker))
|
||||
return
|
||||
}
|
||||
|
||||
setNote("draftTop", large ? estimateTop(range) : undefined)
|
||||
}
|
||||
|
||||
const scheduleComments = () => {
|
||||
requestAnimationFrame(updateComments)
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!document.activeElement || !current.contains(document.activeElement)) {
|
||||
setNote("commenting", null)
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
commentLayout()
|
||||
scheduleComments()
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.defaultPrevented) return
|
||||
if (tabs().active() !== props.tab) return
|
||||
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return
|
||||
if (event.key.toLowerCase() !== "f") return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
find?.focus()
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown, { capture: true })
|
||||
onCleanup(() => window.removeEventListener("keydown", onKeyDown, { capture: true }))
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
path,
|
||||
() => {
|
||||
commentsUi.note.reset()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const focus = comments.focus()
|
||||
const p = path()
|
||||
@@ -278,9 +267,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
const target = fileComments().find((comment) => comment.id === focus.id)
|
||||
if (!target) return
|
||||
|
||||
setNote("openedComment", target.id)
|
||||
setCommenting(null)
|
||||
file.setSelectedLines(p, target.selection)
|
||||
commentsUi.note.openComment(target.id, target.selection, { cancelDraft: true })
|
||||
requestAnimationFrame(() => comments.clearFocus())
|
||||
})
|
||||
|
||||
@@ -371,6 +358,12 @@ export function FileTabContent(props: { tab: string }) {
|
||||
})
|
||||
}
|
||||
|
||||
const cancelCommenting = () => {
|
||||
const p = path()
|
||||
if (p) file.setSelectedLines(p, null)
|
||||
setNote("commenting", null)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => state()?.loaded,
|
||||
@@ -413,99 +406,50 @@ export function FileTabContent(props: { tab: string }) {
|
||||
cancelAnimationFrame(scrollFrame)
|
||||
})
|
||||
|
||||
const renderCode = (source: string, wrapperClass: string) => (
|
||||
<div
|
||||
ref={(el) => {
|
||||
wrap = el
|
||||
scheduleComments()
|
||||
}}
|
||||
class={`relative overflow-hidden ${wrapperClass}`}
|
||||
>
|
||||
const renderFile = (source: string) => (
|
||||
<div class="relative overflow-hidden pb-40">
|
||||
<Dynamic
|
||||
component={codeComponent}
|
||||
component={fileComponent}
|
||||
mode="text"
|
||||
file={{
|
||||
name: path() ?? "",
|
||||
contents: source,
|
||||
cacheKey: cacheKey(),
|
||||
}}
|
||||
enableLineSelection
|
||||
selectedLines={selectedLines()}
|
||||
enableHoverUtility
|
||||
selectedLines={activeSelection()}
|
||||
commentedLines={commentedLines()}
|
||||
onRendered={() => {
|
||||
requestAnimationFrame(restoreScroll)
|
||||
requestAnimationFrame(scheduleComments)
|
||||
}}
|
||||
annotations={commentsUi.annotations()}
|
||||
renderAnnotation={commentsUi.renderAnnotation}
|
||||
renderHoverUtility={commentsUi.renderHoverUtility}
|
||||
onLineSelected={(range: SelectedLineRange | null) => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
file.setSelectedLines(p, range)
|
||||
if (!range) setCommenting(null)
|
||||
commentsUi.onLineSelected(range)
|
||||
}}
|
||||
onLineNumberSelectionEnd={commentsUi.onLineNumberSelectionEnd}
|
||||
onLineSelectionEnd={(range: SelectedLineRange | null) => {
|
||||
if (!range) {
|
||||
setCommenting(null)
|
||||
return
|
||||
}
|
||||
|
||||
setNote("openedComment", null)
|
||||
setCommenting(range)
|
||||
commentsUi.onLineSelectionEnd(range)
|
||||
}}
|
||||
search={search}
|
||||
overflow="scroll"
|
||||
class="select-text"
|
||||
media={{
|
||||
mode: "auto",
|
||||
path: path(),
|
||||
current: state()?.content,
|
||||
onLoad: () => requestAnimationFrame(restoreScroll),
|
||||
onError: (args: { kind: "image" | "audio" | "svg" }) => {
|
||||
if (args.kind !== "svg") return
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.file.loadFailed.title"),
|
||||
})
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<For each={fileComments()}>
|
||||
{(comment) => (
|
||||
<LineCommentView
|
||||
id={comment.id}
|
||||
top={note.positions[comment.id]}
|
||||
open={note.openedComment === comment.id}
|
||||
comment={comment.comment}
|
||||
selection={formatCommentLabel(comment.selection)}
|
||||
onMouseEnter={() => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
file.setSelectedLines(p, comment.selection)
|
||||
}}
|
||||
onClick={() => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
setCommenting(null)
|
||||
setNote("openedComment", (current) => (current === comment.id ? null : comment.id))
|
||||
file.setSelectedLines(p, comment.selection)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={note.commenting}>
|
||||
{(range) => (
|
||||
<Show when={note.draftTop !== undefined}>
|
||||
<LineCommentEditor
|
||||
top={note.draftTop}
|
||||
value={note.draft}
|
||||
selection={formatCommentLabel(range())}
|
||||
onInput={(value) => setNote("draft", value)}
|
||||
onCancel={() => setCommenting(null)}
|
||||
onSubmit={(value) => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
addCommentToContext({ file: p, selection: range(), comment: value, origin: "file" })
|
||||
setCommenting(null)
|
||||
}}
|
||||
onPopoverFocusOut={(e: FocusEvent) => {
|
||||
const current = e.currentTarget as HTMLDivElement
|
||||
const target = e.relatedTarget
|
||||
if (target instanceof Node && current.contains(target)) return
|
||||
|
||||
setTimeout(() => {
|
||||
if (!document.activeElement || !current.contains(document.activeElement)) {
|
||||
setCommenting(null)
|
||||
}
|
||||
}, 0)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -520,36 +464,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
onScroll={handleScroll as any}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={state()?.loaded && isImage()}>
|
||||
<div class="px-6 py-4 pb-40">
|
||||
<img
|
||||
src={imageDataUrl()}
|
||||
alt={path()}
|
||||
class="max-w-full"
|
||||
onLoad={() => requestAnimationFrame(restoreScroll)}
|
||||
/>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded && isSvg()}>
|
||||
<div class="flex flex-col gap-4 px-6 py-4">
|
||||
{renderCode(svgContent() ?? "", "")}
|
||||
<Show when={svgPreviewUrl()}>
|
||||
<div class="flex justify-center pb-40">
|
||||
<img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded && isBinary()}>
|
||||
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="flex flex-col gap-2 max-w-md">
|
||||
<div class="text-14-semibold text-text-strong truncate">{path()?.split("/").pop()}</div>
|
||||
<div class="text-14-regular text-text-weak">{language.t("session.files.binaryContent")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
|
||||
<Match when={state()?.loaded}>{renderFile(contents())}</Match>
|
||||
<Match when={state()?.loading}>
|
||||
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
|
||||
</Match>
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("createOpenReviewFile", () => {
|
||||
|
||||
openReviewFile("src/a.ts")
|
||||
|
||||
expect(calls).toEqual(["show", "tab:src/a.ts", "open:file://src/a.ts", "load:src/a.ts"])
|
||||
expect(calls).toEqual(["show", "load:src/a.ts", "tab:src/a.ts", "open:file://src/a.ts"])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user