Compare commits

...

56 Commits

Author SHA1 Message Date
Adam
f2100dcfd8 fix(app): scroll jacking 2026-02-26 20:41:35 -06:00
Adam
b0b88f6792 fix(app): permission indicator 2026-02-26 20:40:39 -06:00
Adam
e9a7c71141 fix(app): permission notifications 2026-02-26 20:36:10 -06:00
David Hill
4205fbd2aa tweak(app): show keybind on context tab close 2026-02-27 00:29:02 +00:00
Adam
fc52e4b2d3 feat(app): better diff/code comments (#14621)
Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com>
Co-authored-by: David Hill <iamdavidhill@gmail.com>
2026-02-26 18:23:04 -06:00
Kit Langton
9a6bfeb782 refactor(app): dedupe filetree scroll state 2026-02-26 19:20:56 -05:00
David Hill
fa119423ec tweak(app): align review changes select height 2026-02-27 00:18:14 +00:00
David Hill
bf442a50c0 fix(ui): mute inactive file tab icons 2026-02-27 00:18:14 +00:00
David Hill
09e1b98bc6 tweak(ui): max-width on session when the review is closed but the file tree is open 2026-02-27 00:18:14 +00:00
Adam
37d42595cf fix: test 2026-02-26 18:11:05 -06:00
Adam
adabad19f1 Revert "fix(ui): prevent filename and diff count overlap in session changes (#14773)"
This reverts commit a0b3bbffd5.
2026-02-26 18:11:05 -06:00
David Hill
7a74be3b47 tweak(ui): add border to filetree on scroll 2026-02-26 23:18:35 +00:00
David Hill
c95febb1d5 tui: fix session tab alignment in compact view to prevent vertical overflow 2026-02-26 22:54:49 +00:00
opencode-agent[bot]
9736fce8fc chore: update nix node_modules hashes 2026-02-26 22:18:33 +00:00
Adam
05d77b7d47 chore: storybook (#15285)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
2026-02-26 16:05:04 -06:00
Adam
8c484a05b8 fix(app): terminal issues 2026-02-26 15:52:35 -06:00
Shoubhit Dash
a0b3bbffd5 fix(ui): prevent filename and diff count overlap in session changes (#14773) 2026-02-26 15:42:24 -06:00
Shoubhit Dash
270d084cb1 fix(ui): avoid truncating workspace paths in assistant text (#14584) 2026-02-26 15:41:58 -06:00
Adam
9312867565 feat(app): new tabs styling (#15284)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
2026-02-26 15:39:45 -06:00
Adam
7e6a007c35 feat(app): auto-accept all permissions mode 2026-02-26 15:23:26 -06:00
OpeOginni
5745ee87ba refactor(desktop): enhance project tile interaction with suppress hover functionality (#15214) 2026-02-26 11:00:11 -06:00
Niu Shuai
08f056d412 docs: Sync zh_CN docs with English Version (#15228) 2026-02-26 10:59:45 -06:00
Frank
96ca0de3bc wip: zen 2026-02-26 11:17:19 -05:00
adamelmore
b4d0090e00 chore: fix flaky test 2026-02-26 08:53:40 -06:00
Adam
05ac0a73e1 fix(app): simplify review layout (#14953)
Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com>
2026-02-26 08:51:58 -06:00
David Hill
7453e78b35 feat: opencode go provider list (#15203) 2026-02-26 14:37:33 +00:00
Shoubhit Dash
bb8a1718a6 fix(desktop): restore shell path env for desktop sidecar (#15211) 2026-02-26 18:35:21 +05:30
Filip
6b021658ad fix(app): open in powershell (#15112) 2026-02-26 16:39:55 +08:00
opencode
799b2623cb release: v1.2.15 2026-02-26 08:22:25 +00:00
Luke Parker
fce811b52f fix: most segfaults on windows with Bun v1.3.10 stable (#15181) 2026-02-26 17:55:01 +10:00
Stefan
aae75b3cfb fix(app): middle-click tab close in scrollable tab bar (#15081)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-02-26 12:29:08 +05:30
kil-penguin
392a6d993f fix(desktop): remove interactive shell flag from sidecar spawn to prevent hang on macOS (#15136)
Co-authored-by: kil-penguin <hyeonjun@gameduo.net>
2026-02-26 14:02:40 +08:00
Frank
c4ea11fef3 wip: zen 2026-02-25 23:06:16 -05:00
Adam
b8337cddc4 fix(app): permissions and questions from child sessions (#15105)
Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com>
2026-02-26 01:05:08 +00:00
OpeOginni
444178e079 fix(docs): update schema URL in share configuration examples across multiple languages (#15114) 2026-02-25 18:16:39 -05:00
opencode-agent[bot]
4551282a4b chore: generate 2026-02-25 22:55:09 +00:00
Sebastian
9d29d692c6 split tui/server config (#13968) 2026-02-25 23:53:09 +01:00
Frank
1172fa418e wip: zen go 2026-02-25 12:39:50 -05:00
adamelmore
b368181ac9 chore: move glossary 2026-02-25 10:29:05 -06:00
David Hill
7afa48b4ef tweak(ui): keep reasoning inline code subdued in dark mode 2026-02-25 15:18:05 +00:00
Filip
45191ad144 fix(app): keyboard navigation previous/next message (#15047) 2026-02-25 08:57:13 -06:00
Oleksii Pavliuk
2869922696 fix(app): correct Copilot provider description in i18n files (#15071) 2026-02-25 14:56:03 +00:00
Ryan Vogel
e48c1ccf07 chore(workflows): label vouched users and restrict vouch managers (#15075) 2026-02-25 14:56:03 +00:00
opencode-agent[bot]
5e5823ed85 chore: generate 2026-02-25 14:56:03 +00:00
opencode
de2bc25677 release: v1.2.14 2026-02-25 14:55:56 +00:00
Shantur Rathore
79b5ce58e9 feat(core): add message delete endpoint (#14417) 2026-02-25 09:25:26 -05:00
Ayush Thakur
088a81c116 fix: consume stdout concurrently with process exit in auth login (#15058) 2026-02-25 08:22:52 -05:00
opencode
d848c9b6a3 release: v1.2.13 2026-02-25 07:27:19 +00:00
Dax Raad
561f9f5f05 opencode go copy 2026-02-25 01:54:28 -05:00
Dax Raad
3c6c74457d sync 2026-02-25 01:48:10 -05:00
Filip
fc6e7934bd feat(desktop): enhance Windows app resolution and UI loading states (#13320)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2026-02-25 14:39:58 +08:00
Frank
d7500b25b8 zen: go 2026-02-25 01:25:08 -05:00
Frank
5d5f2cfee6 wip: zen go 2026-02-25 00:48:14 -05:00
Frank
1172ebe697 wip: zen go 2026-02-25 00:47:04 -05:00
Frank
d00d98d56a wip: zen go 2026-02-25 00:42:33 -05:00
Frank
6fc5506293 zen: go 2026-02-25 00:31:48 -05:00
339 changed files with 16489 additions and 6106 deletions

View File

@@ -1,10 +1,5 @@
name: "Setup Bun"
description: "Setup Bun with caching and install dependencies"
inputs:
cross-compile:
description: "Pre-cache canary cross-compile binaries for all targets"
required: false
default: "false"
runs:
using: "composite"
steps:
@@ -21,12 +16,13 @@ runs:
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/canary/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT"
echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT"
fi
- name: Setup Bun
@@ -35,54 +31,6 @@ runs:
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }}
bun-download-url: ${{ steps.bun-url.outputs.url }}
- name: Pre-cache canary cross-compile binaries
if: inputs.cross-compile == 'true'
shell: bash
run: |
BUN_VERSION=$(bun --revision)
if echo "$BUN_VERSION" | grep -q "canary"; then
SEMVER=$(echo "$BUN_VERSION" | sed 's/^\([0-9]*\.[0-9]*\.[0-9]*\).*/\1/')
echo "Bun version: $BUN_VERSION (semver: $SEMVER)"
CACHE_DIR="$HOME/.bun/install/cache"
mkdir -p "$CACHE_DIR"
TMP_DIR=$(mktemp -d)
for TARGET in linux-aarch64 linux-x64 linux-x64-baseline linux-aarch64-musl linux-x64-musl linux-x64-musl-baseline darwin-aarch64 darwin-x64 windows-x64 windows-x64-baseline; do
DEST="$CACHE_DIR/bun-${TARGET}-v${SEMVER}"
if [ -f "$DEST" ]; then
echo "Already cached: $DEST"
continue
fi
URL="https://github.com/oven-sh/bun/releases/download/canary/bun-${TARGET}.zip"
echo "Downloading $TARGET from $URL"
if curl -sfL -o "$TMP_DIR/bun.zip" "$URL"; then
unzip -qo "$TMP_DIR/bun.zip" -d "$TMP_DIR"
if echo "$TARGET" | grep -q "windows"; then
BIN_NAME="bun.exe"
else
BIN_NAME="bun"
fi
mv "$TMP_DIR/bun-${TARGET}/$BIN_NAME" "$DEST"
chmod +x "$DEST"
rm -rf "$TMP_DIR/bun-${TARGET}" "$TMP_DIR/bun.zip"
echo "Cached: $DEST"
# baseline bun resolves "bun-darwin-x64" to the baseline cache key
# so copy the modern binary there too
if [ "$TARGET" = "darwin-x64" ]; then
BASELINE_DEST="$CACHE_DIR/bun-darwin-x64-baseline-v${SEMVER}"
if [ ! -f "$BASELINE_DEST" ]; then
cp "$DEST" "$BASELINE_DEST"
echo "Cached (baseline alias): $BASELINE_DEST"
fi
fi
else
echo "Skipped: $TARGET (not available)"
fi
done
rm -rf "$TMP_DIR"
else
echo "Not a canary build ($BUN_VERSION), skipping pre-cache"
fi
- name: Install dependencies
run: bun install
shell: bash

View File

@@ -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"
}
}
}

View File

@@ -77,8 +77,6 @@ jobs:
fetch-tags: true
- uses: ./.github/actions/setup-bun
with:
cross-compile: "true"
- name: Setup git committer
id: committer
@@ -90,7 +88,7 @@ jobs:
- name: Build
id: build
run: |
./packages/opencode/script/build.ts --all
./packages/opencode/script/build.ts
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}

View File

@@ -20,12 +20,10 @@ jobs:
fetch-tags: true
- uses: ./.github/actions/setup-bun
with:
cross-compile: "true"
- name: Build
run: |
./packages/opencode/script/build.ts --all
./packages/opencode/script/build.ts
- name: Upload unsigned Windows CLI
id: upload_unsigned_windows_cli

View File

@@ -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}`);

View File

@@ -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}`);

View File

@@ -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 }}

View File

@@ -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).

233
bun.lock
View File

@@ -25,7 +25,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.2.11",
"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.11",
"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.11",
"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.11",
"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.11",
"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.11",
"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.11",
"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.11",
"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.11",
"version": "1.2.15",
"bin": {
"opencode": "./bin/opencode",
},
@@ -376,7 +376,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.2.11",
"version": "1.2.15",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -396,7 +396,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.2.11",
"version": "1.2.15",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -407,7 +407,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.2.11",
"version": "1.2.15",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -418,9 +418,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.11",
"version": "1.2.15",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -462,7 +483,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.2.11",
"version": "1.2.15",
"dependencies": {
"zod": "catalog:",
},
@@ -473,7 +494,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.2.11",
"version": "1.2.15",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1136,6 +1157,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 +1231,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 +1327,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"],
@@ -1774,6 +1801,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 +1913,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 +1929,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=="],
@@ -2010,7 +2065,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 +2075,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 +2171,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 +2201,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 +2317,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 +2333,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 +2429,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 +2451,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 +2505,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 +2901,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=="],
@@ -3074,6 +3143,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 +3155,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 +3307,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 +3501,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 +3569,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 +3613,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 +3643,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 +3653,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 +3847,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 +3895,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 +3925,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 +3989,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 +4015,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 +4117,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 +4131,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,6 +4207,8 @@
"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=="],
@@ -4260,6 +4363,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 +4593,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 +4739,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,8 +4809,6 @@
"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=="],
@@ -4714,6 +4829,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 +4931,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 +4963,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 +5005,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 +5339,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=="],
@@ -5304,6 +5435,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 +5557,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=="],

View File

@@ -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,

View File

@@ -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-dZoLhWe4smBsOF7WczMySLXSAB1YRO1vfhiOCL1rBf0=",
"aarch64-linux": "sha256-J7nIz1xuVZEHun5WRZkYRySz29B0A8g5g0RRxnIWTYU=",
"aarch64-darwin": "sha256-R2PuhX+EjUBuLE8MF0G0fcUwNaU+5n6V6uVeK89ulzw=",
"x86_64-darwin": "sha256-Bvzfz9TsTpYriZNLSLgpNcNb+BgtkgpjoWqdOtF2IBg="
}
}

View File

@@ -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",

View File

@@ -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")
})

View File

@@ -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()
})

View File

@@ -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] },
)

View File

@@ -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)
}
})
})

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.2.11",
"version": "1.2.15",
"description": "",
"type": "module",
"exports": {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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,
]

View File

@@ -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", () => {

View File

@@ -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",
}
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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"

View File

@@ -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>}

View File

@@ -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()
})
})
})

View File

@@ -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),

View File

@@ -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

View File

@@ -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()
})
})

View File

@@ -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) {

View 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)
})
})

View 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
})
}

View File

@@ -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) {

View File

@@ -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(),

View File

@@ -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() {

View File

@@ -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": "فشل تحميل الملف",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",
@@ -402,10 +404,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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "ファイルの読み込みに失敗しました",

View File

@@ -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": "파일 로드 실패",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "В боковой панели теперь отображаются несколько рабочих деревьев",

View File

@@ -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": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้",

View File

@@ -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": "加载文件失败",

View File

@@ -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": "請先連線提供者以總結此工作階段",

View File

@@ -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}`}
>

View File

@@ -61,6 +61,7 @@ import {
displayName,
errorMessage,
getDraggableId,
latestRootSession,
sortedRootSessions,
syncWorkspaceOrder,
workspaceKey,
@@ -1093,14 +1094,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
}

View File

@@ -1,6 +1,26 @@
import { describe, expect, test } from "bun:test"
import { type Session } from "@opencode-ai/sdk/v2/client"
import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links"
import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers"
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", () => {
@@ -73,6 +93,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()

View File

@@ -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) {

View File

@@ -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

View File

@@ -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}
/>

View File

@@ -107,7 +107,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 +254,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 +379,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 +463,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)
@@ -468,7 +516,8 @@ export default function Page() {
}
onSelect={(option) => option && setStore("changes", option)}
variant="ghost"
size="large"
size="small"
valueClass="text-14-medium"
/>
)
@@ -498,6 +547,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 +571,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 +602,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 +757,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

View File

@@ -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")
})
})

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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())
})
@@ -419,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={cancelCommenting}
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)) {
cancelCommenting()
}
}, 0)
}}
/>
</Show>
)}
</Show>
</div>
)
@@ -526,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>

View File

@@ -2,6 +2,7 @@ import { For, createEffect, createMemo, on, onCleanup, Show, type JSX } from "so
import { createStore, produce } from "solid-js/store"
import { useNavigate, useParams } from "@solidjs/router"
import { Button } from "@opencode-ai/ui/button"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
@@ -9,8 +10,9 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { InlineInput } from "@opencode-ai/ui/inline-input"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import type { UserMessage } from "@opencode-ai/sdk/v2"
import type { Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path"
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
import { SessionContextUsage } from "@/components/session-context-usage"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -18,6 +20,35 @@ import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
type MessageComment = {
path: string
comment: string
selection?: {
startLine: number
endLine: number
}
}
const messageComments = (parts: Part[]): MessageComment[] =>
parts.flatMap((part) => {
if (part.type !== "text" || !(part as TextPart).synthetic) return []
const next = readCommentMetadata(part.metadata) ?? parseCommentNote(part.text)
if (!next) return []
return [
{
path: next.path,
comment: next.comment,
selection: next.selection
? {
startLine: next.selection.startLine,
endLine: next.selection.endLine,
}
: undefined,
},
]
})
const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
const current = target instanceof Element ? target : undefined
@@ -376,6 +407,7 @@ export function MessageTimeline(props: {
>
<Show when={showHeader()}>
<div
data-session-title
classList={{
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
"w-full": true,
@@ -521,34 +553,67 @@ export function MessageTimeline(props: {
</div>
</Show>
<For each={props.renderedUserMessages}>
{(message) => (
<div
id={props.anchor(message.id)}
data-message-id={message.id}
ref={(el) => {
props.onRegisterMessage(el, message.id)
onCleanup(() => props.onUnregisterMessage(message.id))
}}
classList={{
"min-w-0 w-full max-w-full": true,
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
}}
>
<SessionTurn
sessionID={sessionID() ?? ""}
messageID={message.id}
lastUserMessageID={props.lastUserMessageID}
showReasoningSummaries={settings.general.showReasoningSummaries()}
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
editToolDefaultOpen={settings.general.editToolPartsExpanded()}
classes={{
root: "min-w-0 w-full relative",
content: "flex flex-col justify-between !overflow-visible",
container: "w-full px-4 md:px-5",
{(message) => {
const comments = createMemo(() => messageComments(sync.data.part[message.id] ?? []))
return (
<div
id={props.anchor(message.id)}
data-message-id={message.id}
ref={(el) => {
props.onRegisterMessage(el, message.id)
onCleanup(() => props.onUnregisterMessage(message.id))
}}
/>
</div>
)}
classList={{
"min-w-0 w-full max-w-full": true,
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
}}
>
<Show when={comments().length > 0}>
<div class="w-full px-4 md:px-5 pb-2">
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
<div class="flex w-max min-w-full justify-end gap-2">
<For each={comments()}>
{(comment) => (
<div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
<div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
<FileIcon node={{ path: comment.path, type: "file" }} class="size-3.5 shrink-0" />
<span class="truncate">{getFilename(comment.path)}</span>
<Show when={comment.selection}>
{(selection) => (
<span class="shrink-0 text-text-weak">
{selection().startLine === selection().endLine
? `:${selection().startLine}`
: `:${selection().startLine}-${selection().endLine}`}
</span>
)}
</Show>
</div>
<div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words">
{comment.comment}
</div>
</div>
)}
</For>
</div>
</div>
</div>
</Show>
<SessionTurn
sessionID={sessionID() ?? ""}
messageID={message.id}
lastUserMessageID={props.lastUserMessageID}
showReasoningSummaries={settings.general.showReasoningSummaries()}
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
editToolDefaultOpen={settings.general.editToolPartsExpanded()}
classes={{
root: "min-w-0 w-full relative",
content: "flex flex-col justify-between !overflow-visible",
container: "w-full px-4 md:px-5",
}}
/>
</div>
)
}}
</For>
</div>
</ScrollView>

View File

@@ -1,7 +1,11 @@
import { createEffect, on, onCleanup, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import type { FileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/ui/session-review"
import type {
SessionReviewCommentActions,
SessionReviewCommentDelete,
SessionReviewCommentUpdate,
} from "@opencode-ai/ui/session-review"
import type { SelectedLineRange } from "@/context/file"
import { useSDK } from "@/context/sdk"
import { useLayout } from "@/context/layout"
@@ -18,6 +22,9 @@ export interface SessionReviewTabProps {
onDiffStyleChange?: (style: DiffStyle) => void
onViewFile?: (file: string) => void
onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void
onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void
onLineCommentDelete?: (comment: SessionReviewCommentDelete) => void
lineCommentActions?: SessionReviewCommentActions
comments?: LineComment[]
focusedComment?: { file: string; id: string } | null
onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
@@ -31,38 +38,8 @@ export interface SessionReviewTabProps {
}
export function StickyAddButton(props: { children: JSX.Element }) {
const [state, setState] = createStore({ stuck: false })
let button: HTMLDivElement | undefined
createEffect(() => {
const node = button
if (!node) return
const scroll = node.parentElement
if (!scroll) return
const handler = () => {
const rect = node.getBoundingClientRect()
const scrollRect = scroll.getBoundingClientRect()
setState("stuck", rect.right >= scrollRect.right && scroll.scrollWidth > scroll.clientWidth)
}
scroll.addEventListener("scroll", handler, { passive: true })
const observer = new ResizeObserver(handler)
observer.observe(scroll)
handler()
onCleanup(() => {
scroll.removeEventListener("scroll", handler)
observer.disconnect()
})
})
return (
<div
ref={button}
class="bg-background-base h-full shrink-0 sticky right-0 z-10 flex items-center justify-center border-b border-border-weak-base px-3"
classList={{ "border-l": state.stuck }}
>
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
{props.children}
</div>
)
@@ -70,10 +47,11 @@ export function StickyAddButton(props: { children: JSX.Element }) {
export function SessionReviewTab(props: SessionReviewTabProps) {
let scroll: HTMLDivElement | undefined
let frame: number | undefined
let pending: { x: number; y: number } | undefined
let restoreFrame: number | undefined
let userInteracted = false
const sdk = useSDK()
const layout = useLayout()
const readFile = async (path: string) => {
return sdk.client.file
@@ -85,48 +63,81 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
})
}
const restoreScroll = () => {
const handleInteraction = () => {
userInteracted = true
}
const doRestore = () => {
restoreFrame = undefined
const el = scroll
if (!el) return
if (!el || !layout.ready() || userInteracted) return
if (el.clientHeight === 0 || el.clientWidth === 0) return
const s = props.view().scroll("review")
if (!s) return
if (!s || (s.x === 0 && s.y === 0)) return
if (el.scrollTop !== s.y) el.scrollTop = s.y
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
const maxY = Math.max(0, el.scrollHeight - el.clientHeight)
const maxX = Math.max(0, el.scrollWidth - el.clientWidth)
const targetY = Math.min(s.y, maxY)
const targetX = Math.min(s.x, maxX)
if (el.scrollTop !== targetY) el.scrollTop = targetY
if (el.scrollLeft !== targetX) el.scrollLeft = targetX
}
const queueRestore = () => {
if (userInteracted || restoreFrame !== undefined) return
restoreFrame = requestAnimationFrame(doRestore)
}
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
pending = {
x: event.currentTarget.scrollLeft,
y: event.currentTarget.scrollTop,
}
if (frame !== undefined) return
if (!layout.ready() || !userInteracted) return
frame = requestAnimationFrame(() => {
frame = undefined
const el = event.currentTarget
if (el.clientHeight === 0 || el.clientWidth === 0) return
const next = pending
pending = undefined
if (!next) return
props.view().setScroll("review", next)
props.view().setScroll("review", {
x: el.scrollLeft,
y: el.scrollTop,
})
}
createEffect(
on(
() => props.diffs().length,
() => {
requestAnimationFrame(restoreScroll)
() => queueRestore(),
{ defer: true },
),
)
createEffect(
on(
() => props.diffStyle,
() => queueRestore(),
{ defer: true },
),
)
createEffect(
on(
() => layout.ready(),
(ready) => {
if (!ready) return
queueRestore()
},
{ defer: true },
),
)
onCleanup(() => {
if (frame === undefined) return
cancelAnimationFrame(frame)
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
if (scroll) {
scroll.removeEventListener("wheel", handleInteraction)
scroll.removeEventListener("pointerdown", handleInteraction)
scroll.removeEventListener("touchstart", handleInteraction)
scroll.removeEventListener("keydown", handleInteraction)
}
})
return (
@@ -135,11 +146,15 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
empty={props.empty}
scrollRef={(el) => {
scroll = el
el.addEventListener("wheel", handleInteraction, { passive: true, capture: true })
el.addEventListener("pointerdown", handleInteraction, { passive: true, capture: true })
el.addEventListener("touchstart", handleInteraction, { passive: true, capture: true })
el.addEventListener("keydown", handleInteraction, { passive: true, capture: true })
props.onScrollRef?.(el)
restoreScroll()
queueRestore()
}}
onScroll={handleScroll}
onDiffRendered={() => requestAnimationFrame(restoreScroll)}
onDiffRendered={queueRestore}
open={props.view().review.open()}
onOpenChange={props.view().review.setOpen}
classes={{
@@ -154,6 +169,9 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
focusedFile={props.focusedFile}
readFile={readFile}
onLineComment={props.onLineComment}
onLineCommentUpdate={props.onLineCommentUpdate}
onLineCommentDelete={props.onLineCommentDelete}
lineCommentActions={props.lineCommentActions}
comments={props.comments}
focusedComment={props.focusedComment}
onFocusedCommentChange={props.onFocusedCommentChange}

View File

@@ -4,7 +4,7 @@ import { createMediaQuery } from "@solid-primitives/media"
import { useParams } from "@solidjs/router"
import { Tabs } from "@opencode-ai/ui/tabs"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo"
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
@@ -47,7 +47,7 @@ export function SessionSidePanel(props: {
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened()))
const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened())
const reviewTab = createMemo(() => isDesktop())
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
@@ -145,8 +145,17 @@ export function SessionSidePanel(props: {
const [store, setStore] = createStore({
activeDraggable: undefined as string | undefined,
fileTreeScrolled: false,
})
let changesEl: HTMLDivElement | undefined
let allEl: HTMLDivElement | undefined
const syncFileTreeScrolled = (el?: HTMLDivElement) => {
const next = (el?.scrollTop ?? 0) > 0
setStore("fileTreeScrolled", (current) => (current === next ? current : next))
}
const handleDragStart = (event: unknown) => {
const id = getDraggableId(event)
if (!id) return
@@ -167,6 +176,11 @@ export function SessionSidePanel(props: {
setStore("activeDraggable", undefined)
}
createEffect(() => {
if (!layout.fileTree.opened()) return
syncFileTreeScrolled(fileTreeTab() === "changes" ? changesEl : allEl)
})
createEffect(() => {
if (!file.ready()) return
@@ -202,133 +216,128 @@ export function SessionSidePanel(props: {
>
<Show when={reviewOpen()}>
<div class="flex-1 min-w-0 h-full">
<Show
when={layout.fileTree.opened() && fileTreeTab() === "changes"}
fallback={
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={activeTab()} onChange={openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List
ref={(el: HTMLDivElement) => {
const stop = createFileTabListSync({ el, contextOpen })
onCleanup(stop)
}}
>
<Show when={reviewTab()}>
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
<div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div>
<Show when={hasReview()}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{reviewCount()}
</div>
</Show>
</div>
</Tabs.Trigger>
</Show>
<Show when={contextOpen()}>
<Tabs.Trigger
value="context"
closeButton={
<Tooltip value={language.t("common.closeTab")} placement="bottom">
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => tabs().close("context")}
aria-label={language.t("common.closeTab")}
/>
</Tooltip>
}
hideCloseButton
onMiddleClick={() => tabs().close("context")}
>
<div class="flex items-center gap-2">
<SessionContextUsage variant="indicator" />
<div>{language.t("session.tab.context")}</div>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={openedTabs()}>
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
</SortableProvider>
<StickyAddButton>
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={activeTab()} onChange={openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List
ref={(el: HTMLDivElement) => {
const stop = createFileTabListSync({ el, contextOpen })
onCleanup(stop)
}}
>
<Show when={reviewTab()}>
<Tabs.Trigger value="review">
<div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div>
<Show when={hasReview()}>
<div>{reviewCount()}</div>
</Show>
</div>
</Tabs.Trigger>
</Show>
<Show when={contextOpen()}>
<Tabs.Trigger
value="context"
closeButton={
<TooltipKeybind
title={language.t("command.file.open")}
keybind={command.keybind("file.open")}
class="flex items-center"
title={language.t("common.closeTab")}
keybind={command.keybind("tab.close")}
placement="bottom"
gutter={10}
>
<IconButton
icon="plus-small"
icon="close-small"
variant="ghost"
iconSize="large"
onClick={() =>
dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
}
aria-label={language.t("command.file.open")}
class="h-5 w-5"
onClick={() => tabs().close("context")}
aria-label={language.t("common.closeTab")}
/>
</TooltipKeybind>
</StickyAddButton>
</Tabs.List>
</div>
<Show when={reviewTab()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content>
</Show>
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">
{language.t("session.files.selectToOpen")}
</div>
</div>
}
hideCloseButton
onMiddleClick={() => tabs().close("context")}
>
<div class="flex items-center gap-2">
<SessionContextUsage variant="indicator" />
<div>{language.t("session.tab.context")}</div>
</div>
</Show>
</Tabs.Content>
</Tabs.Trigger>
</Show>
<SortableProvider ids={openedTabs()}>
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
</SortableProvider>
<StickyAddButton>
<TooltipKeybind
title={language.t("command.file.open")}
keybind={command.keybind("file.open")}
class="flex items-center"
>
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
class="!rounded-md"
onClick={() => dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)}
aria-label={language.t("command.file.open")}
/>
</TooltipKeybind>
</StickyAddButton>
</Tabs.List>
</div>
<Show when={contextOpen()}>
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "context"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionContextTab />
</div>
</Show>
</Tabs.Content>
</Show>
<Show when={reviewTab()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content>
</Show>
<Show when={activeFileTab()} keyed>
{(tab) => <FileTabContent tab={tab} />}
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">
{language.t("session.files.selectToOpen")}
</div>
</div>
</div>
</Show>
</Tabs.Content>
<Show when={contextOpen()}>
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "context"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionContextTab />
</div>
</Show>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable} keyed>
{(tab) => {
const path = createMemo(() => file.pathFromTab(tab))
return (
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
}
>
{props.reviewPanel()}
</Show>
</Tabs.Content>
</Show>
<Show when={activeFileTab()} keyed>
{(tab) => <FileTabContent tab={tab} />}
</Show>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable} keyed>
{(tab) => {
const path = createMemo(() => file.pathFromTab(tab))
return (
<div data-component="tabs-drag-preview">
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</Show>
@@ -345,7 +354,7 @@ export function SessionSidePanel(props: {
class="h-full"
data-scope="filetree"
>
<Tabs.List>
<Tabs.List data-scrolled={store.fileTreeScrolled ? "" : undefined}>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
{reviewCount()}{" "}
{language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
@@ -354,7 +363,12 @@ export function SessionSidePanel(props: {
{language.t("session.files.all")}
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
<Tabs.Content
value="changes"
ref={(el: HTMLDivElement) => (changesEl = el)}
onScroll={(e: UIEvent & { currentTarget: HTMLDivElement }) => syncFileTreeScrolled(e.currentTarget)}
class="bg-background-stronger px-3 py-0"
>
<Switch>
<Match when={hasReview()}>
<Show
@@ -383,7 +397,12 @@ export function SessionSidePanel(props: {
</Match>
</Switch>
</Tabs.Content>
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
<Tabs.Content
value="all"
ref={(el: HTMLDivElement) => (allEl = el)}
onScroll={(e: UIEvent & { currentTarget: HTMLDivElement }) => syncFileTreeScrolled(e.currentTarget)}
class="bg-background-stronger px-3 py-0"
>
<FileTree
path=""
modified={diffFiles()}

View File

@@ -45,7 +45,9 @@ export const useSessionHashScroll = (input: {
const a = el.getBoundingClientRect()
const b = root.getBoundingClientRect()
const top = a.top - b.top + root.scrollTop
const sticky = root.querySelector("[data-session-title]")
const inset = sticky instanceof HTMLElement ? sticky.offsetHeight : 0
const top = Math.max(0, a.top - b.top + root.scrollTop - inset)
root.scrollTo({ top, behavior })
return true
}

View File

@@ -0,0 +1,88 @@
import type { FileSelection } from "@/context/file"
export type PromptComment = {
path: string
selection?: FileSelection
comment: string
preview?: string
origin?: "review" | "file"
}
function selection(selection: unknown) {
if (!selection || typeof selection !== "object") return undefined
const startLine = Number((selection as FileSelection).startLine)
const startChar = Number((selection as FileSelection).startChar)
const endLine = Number((selection as FileSelection).endLine)
const endChar = Number((selection as FileSelection).endChar)
if (![startLine, startChar, endLine, endChar].every(Number.isFinite)) return undefined
return {
startLine,
startChar,
endLine,
endChar,
} satisfies FileSelection
}
export function createCommentMetadata(input: PromptComment) {
return {
opencodeComment: {
path: input.path,
selection: input.selection,
comment: input.comment,
preview: input.preview,
origin: input.origin,
},
}
}
export function readCommentMetadata(value: unknown) {
if (!value || typeof value !== "object") return
const meta = (value as { opencodeComment?: unknown }).opencodeComment
if (!meta || typeof meta !== "object") return
const path = (meta as { path?: unknown }).path
const comment = (meta as { comment?: unknown }).comment
if (typeof path !== "string" || typeof comment !== "string") return
const preview = (meta as { preview?: unknown }).preview
const origin = (meta as { origin?: unknown }).origin
return {
path,
selection: selection((meta as { selection?: unknown }).selection),
comment,
preview: typeof preview === "string" ? preview : undefined,
origin: origin === "review" || origin === "file" ? origin : undefined,
} satisfies PromptComment
}
export function formatCommentNote(input: { path: string; selection?: FileSelection; comment: string }) {
const start = input.selection ? Math.min(input.selection.startLine, input.selection.endLine) : undefined
const end = input.selection ? Math.max(input.selection.startLine, input.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 ${input.path}: ${input.comment}`
}
export function parseCommentNote(text: string) {
const match = text.match(
/^The user made the following comment regarding (this file|line (\d+)|lines (\d+) through (\d+)) of (.+?): ([\s\S]+)$/,
)
if (!match) return
const start = match[2] ? Number(match[2]) : match[3] ? Number(match[3]) : undefined
const end = match[2] ? Number(match[2]) : match[4] ? Number(match[4]) : undefined
return {
path: match[5],
selection:
start !== undefined && end !== undefined
? {
startLine: start,
startChar: 0,
endLine: end,
endChar: 0,
}
: undefined,
comment: match[6],
} satisfies PromptComment
}

View File

@@ -1,13 +1,13 @@
{
"name": "@opencode-ai/console-app",
"version": "1.2.11",
"version": "1.2.15",
"type": "module",
"license": "MIT",
"scripts": {
"typecheck": "tsgo --noEmit",
"dev": "vite dev --host 0.0.0.0",
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev",
"build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json",
"build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json ./.output/public/tui.json",
"start": "vite start"
},
"dependencies": {

View File

@@ -344,8 +344,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "كتابة الكاش",
"workspace.usage.breakdown.output": "الخرج",
"workspace.usage.breakdown.reasoning": "المنطق",
"workspace.usage.subscription": "الاشتراك (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "التكلفة",
@@ -491,21 +491,26 @@ export const dict = {
"workspace.lite.time.minute": "دقيقة",
"workspace.lite.time.minutes": "دقائق",
"workspace.lite.time.fewSeconds": "بضع ثوان",
"workspace.lite.subscription.title": "اشتراك Lite",
"workspace.lite.subscription.message": "أنت مشترك في OpenCode Lite.",
"workspace.lite.subscription.title": "اشتراك Go",
"workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.",
"workspace.lite.subscription.manage": "إدارة الاشتراك",
"workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد",
"workspace.lite.subscription.weeklyUsage": "الاستخدام الأسبوعي",
"workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري",
"workspace.lite.subscription.resetsIn": "إعادة تعيين في",
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
"workspace.lite.other.title": "اشتراك Lite",
"workspace.lite.subscription.selectProvider":
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
"workspace.lite.other.title": "اشتراك Go",
"workspace.lite.other.message":
"عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Lite. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.",
"workspace.lite.promo.title": "OpenCode Lite",
"عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"احصل على وصول إلى أفضل النماذج المفتوحة — Kimi K2.5، و GLM-5، و MiniMax M2.5 — مع حدود استخدام سخية مقابل $10 شهريًا.",
"workspace.lite.promo.subscribe": "الاشتراك في Lite",
"OpenCode Go هو اشتراك بسعر $10 شهريًا يوفر وصولاً موثوقًا إلى نماذج البرمجة المفتوحة الشائعة مع حدود استخدام سخية.",
"workspace.lite.promo.modelsTitle": "ما يتضمنه",
"workspace.lite.promo.footer":
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.",
"workspace.lite.promo.subscribe": "الاشتراك في Go",
"workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...",
"download.title": "OpenCode | تنزيل",

View File

@@ -349,8 +349,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Escrita em Cache",
"workspace.usage.breakdown.output": "Saída",
"workspace.usage.breakdown.reasoning": "Raciocínio",
"workspace.usage.subscription": "assinatura (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Custo",
@@ -497,21 +497,26 @@ export const dict = {
"workspace.lite.time.minute": "minuto",
"workspace.lite.time.minutes": "minutos",
"workspace.lite.time.fewSeconds": "alguns segundos",
"workspace.lite.subscription.title": "Assinatura Lite",
"workspace.lite.subscription.message": "Você assina o OpenCode Lite.",
"workspace.lite.subscription.title": "Assinatura Go",
"workspace.lite.subscription.message": "Você assina o OpenCode Go.",
"workspace.lite.subscription.manage": "Gerenciar Assinatura",
"workspace.lite.subscription.rollingUsage": "Uso Contínuo",
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
"workspace.lite.subscription.monthlyUsage": "Uso Mensal",
"workspace.lite.subscription.resetsIn": "Reinicia em",
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
"workspace.lite.other.title": "Assinatura Lite",
"workspace.lite.subscription.selectProvider":
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
"workspace.lite.other.title": "Assinatura Go",
"workspace.lite.other.message":
"Outro membro neste workspace já assina o OpenCode Lite. Apenas um membro por workspace pode assinar.",
"workspace.lite.promo.title": "OpenCode Lite",
"Outro membro neste workspace já assina o OpenCode Go. Apenas um membro por workspace pode assinar.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"Tenha acesso aos melhores modelos abertos — Kimi K2.5, GLM-5 e MiniMax M2.5 — com limites de uso generosos por $10 por mês.",
"workspace.lite.promo.subscribe": "Assinar Lite",
"O OpenCode Go é uma assinatura de $10 por mês que fornece acesso confiável a modelos abertos de codificação populares com limites de uso generosos.",
"workspace.lite.promo.modelsTitle": "O que está incluído",
"workspace.lite.promo.footer":
"O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Preços e limites de uso podem mudar conforme aprendemos com o uso inicial e feedback.",
"workspace.lite.promo.subscribe": "Assinar Go",
"workspace.lite.promo.subscribing": "Redirecionando...",
"download.title": "OpenCode | Baixar",

View File

@@ -347,8 +347,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Cache skriv",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Ræsonnement",
"workspace.usage.subscription": "abonnement (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Omkostninger",
@@ -495,21 +495,26 @@ export const dict = {
"workspace.lite.time.minute": "minut",
"workspace.lite.time.minutes": "minutter",
"workspace.lite.time.fewSeconds": "et par sekunder",
"workspace.lite.subscription.title": "Lite-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.",
"workspace.lite.subscription.title": "Go-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løbende forbrug",
"workspace.lite.subscription.weeklyUsage": "Ugentligt forbrug",
"workspace.lite.subscription.monthlyUsage": "Månedligt forbrug",
"workspace.lite.subscription.resetsIn": "Nulstiller i",
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
"workspace.lite.other.title": "Lite-abonnement",
"workspace.lite.subscription.selectProvider":
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
"workspace.lite.other.title": "Go-abonnement",
"workspace.lite.other.message":
"Et andet medlem i dette workspace abonnerer allerede på OpenCode Lite. Kun ét medlem pr. workspace kan abonnere.",
"workspace.lite.promo.title": "OpenCode Lite",
"Et andet medlem i dette workspace abonnerer allerede på OpenCode Go. Kun ét medlem pr. workspace kan abonnere.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"Få adgang til de bedste åbne modeller — Kimi K2.5, GLM-5 og MiniMax M2.5 — med generøse forbrugsgrænser for $10 om måneden.",
"workspace.lite.promo.subscribe": "Abonner på Lite",
"OpenCode Go er et abonnement til $10 om måneden, der giver pålidelig adgang til populære åbne kodningsmodeller med generøse forbrugsgrænser.",
"workspace.lite.promo.modelsTitle": "Hvad er inkluderet",
"workspace.lite.promo.footer":
"Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af tidlig brug og feedback.",
"workspace.lite.promo.subscribe": "Abonner på Go",
"workspace.lite.promo.subscribing": "Omdirigerer...",
"download.title": "OpenCode | Download",

View File

@@ -349,8 +349,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Cache Write",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Reasoning",
"workspace.usage.subscription": "Abonnement (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Kosten",
@@ -497,21 +497,26 @@ export const dict = {
"workspace.lite.time.minute": "Minute",
"workspace.lite.time.minutes": "Minuten",
"workspace.lite.time.fewSeconds": "einige Sekunden",
"workspace.lite.subscription.title": "Lite-Abonnement",
"workspace.lite.subscription.message": "Du hast OpenCode Lite abonniert.",
"workspace.lite.subscription.title": "Go-Abonnement",
"workspace.lite.subscription.message": "Du hast OpenCode Go abonniert.",
"workspace.lite.subscription.manage": "Abo verwalten",
"workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung",
"workspace.lite.subscription.weeklyUsage": "Wöchentliche Nutzung",
"workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung",
"workspace.lite.subscription.resetsIn": "Setzt zurück in",
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
"workspace.lite.other.title": "Lite-Abonnement",
"workspace.lite.subscription.selectProvider":
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
"workspace.lite.other.title": "Go-Abonnement",
"workspace.lite.other.message":
"Ein anderes Mitglied in diesem Workspace hat OpenCode Lite bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.",
"workspace.lite.promo.title": "OpenCode Lite",
"Ein anderes Mitglied in diesem Workspace hat OpenCode Go bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"Erhalte Zugriff auf die besten offenen Modelle — Kimi K2.5, GLM-5 und MiniMax M2.5 — mit großzügigen Nutzungslimits für $10 pro Monat.",
"workspace.lite.promo.subscribe": "Lite abonnieren",
"OpenCode Go ist ein Abonnement für $10 pro Monat, das zuverlässigen Zugriff auf beliebte offene Coding-Modelle mit großzügigen Nutzungslimits bietet.",
"workspace.lite.promo.modelsTitle": "Was enthalten ist",
"workspace.lite.promo.footer":
"Der Plan wurde hauptsächlich für internationale Nutzer entwickelt, wobei die Modelle in den USA, der EU und Singapur gehostet werden, um einen stabilen weltweiten Zugriff zu gewährleisten. Preise und Nutzungslimits können sich ändern, während wir aus der frühen Nutzung und dem Feedback lernen.",
"workspace.lite.promo.subscribe": "Go abonnieren",
"workspace.lite.promo.subscribing": "Leite weiter...",
"download.title": "OpenCode | Download",

View File

@@ -341,8 +341,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Cache Write",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Reasoning",
"workspace.usage.subscription": "subscription (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Cost",
@@ -489,21 +489,26 @@ export const dict = {
"workspace.lite.time.minute": "minute",
"workspace.lite.time.minutes": "minutes",
"workspace.lite.time.fewSeconds": "a few seconds",
"workspace.lite.subscription.title": "Lite Subscription",
"workspace.lite.subscription.message": "You are subscribed to OpenCode Lite.",
"workspace.lite.subscription.title": "Go Subscription",
"workspace.lite.subscription.message": "You are subscribed to OpenCode Go.",
"workspace.lite.subscription.manage": "Manage Subscription",
"workspace.lite.subscription.rollingUsage": "Rolling Usage",
"workspace.lite.subscription.weeklyUsage": "Weekly Usage",
"workspace.lite.subscription.monthlyUsage": "Monthly Usage",
"workspace.lite.subscription.resetsIn": "Resets in",
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
"workspace.lite.other.title": "Lite Subscription",
"workspace.lite.subscription.selectProvider":
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
"workspace.lite.other.title": "Go Subscription",
"workspace.lite.other.message":
"Another member in this workspace is already subscribed to OpenCode Lite. Only one member per workspace can subscribe.",
"workspace.lite.promo.title": "OpenCode Lite",
"Another member in this workspace is already subscribed to OpenCode Go. Only one member per workspace can subscribe.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"Get access to the best open models — Kimi K2.5, GLM-5, and MiniMax M2.5 — with generous usage limits for $10 per month.",
"workspace.lite.promo.subscribe": "Subscribe to Lite",
"OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models with generous usage limits.",
"workspace.lite.promo.modelsTitle": "What's Included",
"workspace.lite.promo.footer":
"The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Pricing and usage limits may change as we learn from early usage and feedback.",
"workspace.lite.promo.subscribe": "Subscribe to Go",
"workspace.lite.promo.subscribing": "Redirecting...",
"download.title": "OpenCode | Download",

View File

@@ -350,8 +350,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Escritura de Caché",
"workspace.usage.breakdown.output": "Salida",
"workspace.usage.breakdown.reasoning": "Razonamiento",
"workspace.usage.subscription": "suscripción (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Costo",
@@ -498,21 +498,26 @@ export const dict = {
"workspace.lite.time.minute": "minuto",
"workspace.lite.time.minutes": "minutos",
"workspace.lite.time.fewSeconds": "unos pocos segundos",
"workspace.lite.subscription.title": "Suscripción Lite",
"workspace.lite.subscription.message": "Estás suscrito a OpenCode Lite.",
"workspace.lite.subscription.title": "Suscripción Go",
"workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.",
"workspace.lite.subscription.manage": "Gestionar Suscripción",
"workspace.lite.subscription.rollingUsage": "Uso Continuo",
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
"workspace.lite.subscription.monthlyUsage": "Uso Mensual",
"workspace.lite.subscription.resetsIn": "Se reinicia en",
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
"workspace.lite.other.title": "Suscripción Lite",
"workspace.lite.subscription.selectProvider":
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
"workspace.lite.other.title": "Suscripción Go",
"workspace.lite.other.message":
"Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Lite. Solo un miembro por espacio de trabajo puede suscribirse.",
"workspace.lite.promo.title": "OpenCode Lite",
"Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Go. Solo un miembro por espacio de trabajo puede suscribirse.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"Obtén acceso a los mejores modelos abiertos — Kimi K2.5, GLM-5 y MiniMax M2.5 — con generosos límites de uso por $10 al mes.",
"workspace.lite.promo.subscribe": "Suscribirse a Lite",
"OpenCode Go es una suscripción de $10 al mes que proporciona acceso confiable a modelos de codificación abiertos populares con generosos límites de uso.",
"workspace.lite.promo.modelsTitle": "Qué incluye",
"workspace.lite.promo.footer":
"El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., la UE y Singapur para un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y los comentarios.",
"workspace.lite.promo.subscribe": "Suscribirse a Go",
"workspace.lite.promo.subscribing": "Redirigiendo...",
"download.title": "OpenCode | Descargar",

View File

@@ -355,8 +355,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Écriture cache",
"workspace.usage.breakdown.output": "Sortie",
"workspace.usage.breakdown.reasoning": "Raisonnement",
"workspace.usage.subscription": "abonnement ({{amount}} $)",
"workspace.usage.lite": "lite ({{amount}} $)",
"workspace.usage.subscription": "Black ({{amount}} $)",
"workspace.usage.lite": "Go ({{amount}} $)",
"workspace.usage.byok": "BYOK ({{amount}} $)",
"workspace.cost.title": "Coût",
@@ -506,8 +506,8 @@ export const dict = {
"workspace.lite.time.minute": "minute",
"workspace.lite.time.minutes": "minutes",
"workspace.lite.time.fewSeconds": "quelques secondes",
"workspace.lite.subscription.title": "Abonnement Lite",
"workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Lite.",
"workspace.lite.subscription.title": "Abonnement Go",
"workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.",
"workspace.lite.subscription.manage": "Gérer l'abonnement",
"workspace.lite.subscription.rollingUsage": "Utilisation glissante",
"workspace.lite.subscription.weeklyUsage": "Utilisation hebdomadaire",
@@ -515,13 +515,18 @@ export const dict = {
"workspace.lite.subscription.resetsIn": "Réinitialisation dans",
"workspace.lite.subscription.useBalance":
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
"workspace.lite.other.title": "Abonnement Lite",
"workspace.lite.subscription.selectProvider":
'Sélectionnez "OpenCode Go" comme fournisseur dans votre configuration opencode pour utiliser les modèles Go.',
"workspace.lite.other.title": "Abonnement Go",
"workspace.lite.other.message":
"Un autre membre de cet espace de travail est déjà abonné à OpenCode Lite. Un seul membre par espace de travail peut s'abonner.",
"workspace.lite.promo.title": "OpenCode Lite",
"Un autre membre de cet espace de travail est déjà abonné à OpenCode Go. Un seul membre par espace de travail peut s'abonner.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"Accédez aux meilleurs modèles ouverts — Kimi K2.5, GLM-5 et MiniMax M2.5 — avec des limites d'utilisation généreuses pour 10 $ par mois.",
"workspace.lite.promo.subscribe": "S'abonner à Lite",
"OpenCode Go est un abonnement à 10 $ par mois qui offre un accès fiable aux modèles de codage ouverts populaires avec des limites d'utilisation généreuses.",
"workspace.lite.promo.modelsTitle": "Ce qui est inclus",
"workspace.lite.promo.footer":
"Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Les tarifs et les limites d'utilisation peuvent changer à mesure que nous apprenons des premières utilisations et des commentaires.",
"workspace.lite.promo.subscribe": "S'abonner à Go",
"workspace.lite.promo.subscribing": "Redirection...",
"download.title": "OpenCode | Téléchargement",

View File

@@ -349,8 +349,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Scrittura Cache",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Reasoning",
"workspace.usage.subscription": "abbonamento (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Costo",
@@ -497,21 +497,26 @@ export const dict = {
"workspace.lite.time.minute": "minuto",
"workspace.lite.time.minutes": "minuti",
"workspace.lite.time.fewSeconds": "pochi secondi",
"workspace.lite.subscription.title": "Abbonamento Lite",
"workspace.lite.subscription.message": "Sei abbonato a OpenCode Lite.",
"workspace.lite.subscription.title": "Abbonamento Go",
"workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.",
"workspace.lite.subscription.manage": "Gestisci Abbonamento",
"workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo",
"workspace.lite.subscription.weeklyUsage": "Utilizzo Settimanale",
"workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile",
"workspace.lite.subscription.resetsIn": "Si resetta tra",
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
"workspace.lite.other.title": "Abbonamento Lite",
"workspace.lite.subscription.selectProvider":
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
"workspace.lite.other.title": "Abbonamento Go",
"workspace.lite.other.message":
"Un altro membro in questo workspace è già abbonato a OpenCode Lite. Solo un membro per workspace può abbonarsi.",
"workspace.lite.promo.title": "OpenCode Lite",
"Un altro membro in questo workspace è già abbonato a OpenCode Go. Solo un membro per workspace può abbonarsi.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"Ottieni l'accesso ai migliori modelli aperti — Kimi K2.5, GLM-5 e MiniMax M2.5 — con limiti di utilizzo generosi per $10 al mese.",
"workspace.lite.promo.subscribe": "Abbonati a Lite",
"OpenCode Go è un abbonamento a $10 al mese che fornisce un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.",
"workspace.lite.promo.modelsTitle": "Cosa è incluso",
"workspace.lite.promo.footer":
"Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati in US, EU e Singapore per un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare man mano che impariamo dall'utilizzo iniziale e dal feedback.",
"workspace.lite.promo.subscribe": "Abbonati a Go",
"workspace.lite.promo.subscribing": "Reindirizzamento...",
"download.title": "OpenCode | Download",

View File

@@ -346,8 +346,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "キャッシュ書き込み",
"workspace.usage.breakdown.output": "出力",
"workspace.usage.breakdown.reasoning": "推論",
"workspace.usage.subscription": "サブスクリプション (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "コスト",
@@ -495,21 +495,26 @@ export const dict = {
"workspace.lite.time.minute": "分",
"workspace.lite.time.minutes": "分",
"workspace.lite.time.fewSeconds": "数秒",
"workspace.lite.subscription.title": "Liteサブスクリプション",
"workspace.lite.subscription.message": "あなたは OpenCode Lite を購読しています。",
"workspace.lite.subscription.title": "Goサブスクリプション",
"workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。",
"workspace.lite.subscription.manage": "サブスクリプションの管理",
"workspace.lite.subscription.rollingUsage": "ローリング利用量",
"workspace.lite.subscription.weeklyUsage": "週間利用量",
"workspace.lite.subscription.monthlyUsage": "月間利用量",
"workspace.lite.subscription.resetsIn": "リセットまで",
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
"workspace.lite.other.title": "Liteサブスクリプション",
"workspace.lite.subscription.selectProvider":
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
"workspace.lite.other.title": "Goサブスクリプション",
"workspace.lite.other.message":
"このワークスペースの別のメンバーが既に OpenCode Lite を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。",
"workspace.lite.promo.title": "OpenCode Lite",
"このワークスペースの別のメンバーが既に OpenCode Go を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"月額$10で、十分な利用枠が設けられた最高のオープンモデル — Kimi K2.5、GLM-5、および MiniMax M2.5 — にアクセスできます。",
"workspace.lite.promo.subscribe": "Liteを購読する",
"OpenCode Goは月額$10のサブスクリプションプランで、人気のオープンコーディングモデルへの安定したアクセスを十分な利用枠で提供します。",
"workspace.lite.promo.modelsTitle": "含まれるもの",
"workspace.lite.promo.footer":
"このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。",
"workspace.lite.promo.subscribe": "Goを購読する",
"workspace.lite.promo.subscribing": "リダイレクト中...",
"download.title": "OpenCode | ダウンロード",

View File

@@ -343,8 +343,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "캐시 쓰기",
"workspace.usage.breakdown.output": "출력",
"workspace.usage.breakdown.reasoning": "추론",
"workspace.usage.subscription": "구독 (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "비용",
@@ -490,21 +490,26 @@ export const dict = {
"workspace.lite.time.minute": "분",
"workspace.lite.time.minutes": "분",
"workspace.lite.time.fewSeconds": "몇 초",
"workspace.lite.subscription.title": "Lite 구독",
"workspace.lite.subscription.message": "현재 OpenCode Lite를 구독 중입니다.",
"workspace.lite.subscription.title": "Go 구독",
"workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.",
"workspace.lite.subscription.manage": "구독 관리",
"workspace.lite.subscription.rollingUsage": "롤링 사용량",
"workspace.lite.subscription.weeklyUsage": "주간 사용량",
"workspace.lite.subscription.monthlyUsage": "월간 사용량",
"workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:",
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
"workspace.lite.other.title": "Lite 구독",
"workspace.lite.subscription.selectProvider":
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
"workspace.lite.other.title": "Go 구독",
"workspace.lite.other.message":
"이 워크스페이스의 다른 멤버가 이미 OpenCode Lite를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.",
"workspace.lite.promo.title": "OpenCode Lite",
"이 워크스페이스의 다른 멤버가 이미 OpenCode Go를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"월 $10의 넉넉한 사용 한도로 최고의 오픈 모델인 Kimi K2.5, GLM-5, MiniMax M2.5에 액세스하세요.",
"workspace.lite.promo.subscribe": "Lite 구독하기",
"OpenCode Go는 넉넉한 사용 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공하는 월 $10의 구독입니다.",
"workspace.lite.promo.modelsTitle": "포함 내역",
"workspace.lite.promo.footer":
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.",
"workspace.lite.promo.subscribe": "Go 구독하기",
"workspace.lite.promo.subscribing": "리디렉션 중...",
"download.title": "OpenCode | 다운로드",

View File

@@ -347,8 +347,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Cache Skrevet",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Resonnering",
"workspace.usage.subscription": "abonnement (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Kostnad",
@@ -495,21 +495,26 @@ export const dict = {
"workspace.lite.time.minute": "minutt",
"workspace.lite.time.minutes": "minutter",
"workspace.lite.time.fewSeconds": "noen få sekunder",
"workspace.lite.subscription.title": "Lite-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.",
"workspace.lite.subscription.title": "Go-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løpende bruk",
"workspace.lite.subscription.weeklyUsage": "Ukentlig bruk",
"workspace.lite.subscription.monthlyUsage": "Månedlig bruk",
"workspace.lite.subscription.resetsIn": "Nullstilles om",
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
"workspace.lite.other.title": "Lite-abonnement",
"workspace.lite.subscription.selectProvider":
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
"workspace.lite.other.title": "Go-abonnement",
"workspace.lite.other.message":
"Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Lite. Kun ett medlem per arbeidsområde kan abonnere.",
"workspace.lite.promo.title": "OpenCode Lite",
"Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Go. Kun ett medlem per arbeidsområde kan abonnere.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
" tilgang til de beste åpne modellene — Kimi K2.5, GLM-5 og MiniMax M2.5 — med generøse bruksgrenser for $10 per måned.",
"workspace.lite.promo.subscribe": "Abonner på Lite",
"OpenCode Go er et abonnement til $10 per måned som gir pålitelig tilgang til populære åpne kodemodeller med rause bruksgrenser.",
"workspace.lite.promo.modelsTitle": "Hva som er inkludert",
"workspace.lite.promo.footer":
"Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer fra tidlig bruk og tilbakemeldinger.",
"workspace.lite.promo.subscribe": "Abonner på Go",
"workspace.lite.promo.subscribing": "Omdirigerer...",
"download.title": "OpenCode | Last ned",

View File

@@ -348,8 +348,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Zapis Cache",
"workspace.usage.breakdown.output": "Wyjście",
"workspace.usage.breakdown.reasoning": "Rozumowanie",
"workspace.usage.subscription": "subskrypcja (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Koszt",
@@ -496,21 +496,26 @@ export const dict = {
"workspace.lite.time.minute": "minuta",
"workspace.lite.time.minutes": "minut(y)",
"workspace.lite.time.fewSeconds": "kilka sekund",
"workspace.lite.subscription.title": "Subskrypcja Lite",
"workspace.lite.subscription.message": "Subskrybujesz OpenCode Lite.",
"workspace.lite.subscription.title": "Subskrypcja Go",
"workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.",
"workspace.lite.subscription.manage": "Zarządzaj subskrypcją",
"workspace.lite.subscription.rollingUsage": "Użycie kroczące",
"workspace.lite.subscription.weeklyUsage": "Użycie tygodniowe",
"workspace.lite.subscription.monthlyUsage": "Użycie miesięczne",
"workspace.lite.subscription.resetsIn": "Resetuje się za",
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
"workspace.lite.other.title": "Subskrypcja Lite",
"workspace.lite.subscription.selectProvider":
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
"workspace.lite.other.title": "Subskrypcja Go",
"workspace.lite.other.message":
"Inny członek tego obszaru roboczego już subskrybuje OpenCode Lite. Tylko jeden członek na obszar roboczy może subskrybować.",
"workspace.lite.promo.title": "OpenCode Lite",
"Inny członek tego obszaru roboczego już subskrybuje OpenCode Go. Tylko jeden członek na obszar roboczy może subskrybować.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"Uzyskaj dostęp do najlepszych otwartych modeli — Kimi K2.5, GLM-5 i MiniMax M2.5 — z hojnymi limitami użycia za $10 miesięcznie.",
"workspace.lite.promo.subscribe": "Subskrybuj Lite",
"OpenCode Go to subskrypcja za $10 miesięcznie, która zapewnia niezawodny dostęp do popularnych otwartych modeli do kodowania z hojnymi limitami użycia.",
"workspace.lite.promo.modelsTitle": "Co zawiera",
"workspace.lite.promo.footer":
"Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę analizy wczesnego użycia i zbierania opinii.",
"workspace.lite.promo.subscribe": "Subskrybuj Go",
"workspace.lite.promo.subscribing": "Przekierowywanie...",
"download.title": "OpenCode | Pobierz",

Some files were not shown because too many files have changed in this diff Show More