mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-11 03:14:29 +00:00
Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcd5ff7ebe | ||
|
|
c2e234ec4d | ||
|
|
38f735bfc6 | ||
|
|
a4183c3b2c | ||
|
|
2c234b8d62 | ||
|
|
9f96d8aa78 | ||
|
|
4007e57c52 | ||
|
|
d472512eba | ||
|
|
f6b28b61c7 | ||
|
|
0bf9d66da5 | ||
|
|
eabd78cab6 | ||
|
|
7bc8851fc4 | ||
|
|
af5e405391 | ||
|
|
8a216a6ad5 | ||
|
|
225b72ca36 | ||
|
|
8105f186dc | ||
|
|
4f1bdf1c59 | ||
|
|
472695caca | ||
|
|
469fd43c71 | ||
|
|
24d942349f | ||
|
|
65c236c071 | ||
|
|
d6c5ddd6dc | ||
|
|
e5fe50f7da | ||
|
|
b6beda1569 | ||
|
|
f34b509fe7 | ||
|
|
2a2d800ac4 | ||
|
|
4afb46f571 | ||
|
|
c4d223eb99 | ||
|
|
3fbda54045 | ||
|
|
41ede06b20 | ||
|
|
82ec84982e | ||
|
|
df7b6792cd | ||
|
|
c72d9a473c | ||
|
|
d3688b150a | ||
|
|
e376e1de16 | ||
|
|
c130dd425a | ||
|
|
b298982268 | ||
|
|
47a2b9e8df | ||
|
|
213b823c69 | ||
|
|
c0dc8ea39e | ||
|
|
077ebdbfda | ||
|
|
1780bab1ce | ||
|
|
d35fabf5db | ||
|
|
82f718b3cf | ||
|
|
0eb523631d | ||
|
|
99e15caaf6 | ||
|
|
1e1872aada | ||
|
|
cb481d9ac8 | ||
|
|
0ce0cacb28 | ||
|
|
640d1f1ecc | ||
|
|
2e53697da0 | ||
|
|
71cd59932e | ||
|
|
14db336e3a | ||
|
|
2b9b98e9c2 | ||
|
|
07015aae07 | ||
|
|
972cb01d5c | ||
|
|
a8018dcc43 | ||
|
|
31094cd5a4 | ||
|
|
bcf7a65e36 | ||
|
|
7c80ac072b | ||
|
|
515391e9c7 | ||
|
|
510f595e25 | ||
|
|
1b244bf850 | ||
|
|
c128579cfc | ||
|
|
5f3ab9395f | ||
|
|
fdac21688c | ||
|
|
dd5a601eda | ||
|
|
3eaf6f3baf | ||
|
|
71ef43f9a0 | ||
|
|
8ebb766470 | ||
|
|
46de1ed3b6 | ||
|
|
3c7d5174b3 | ||
|
|
32f72f49a8 | ||
|
|
923e3da973 | ||
|
|
c96c25a72c | ||
|
|
cda7d3dd78 | ||
|
|
9802ceb94f | ||
|
|
62115832f5 | ||
|
|
496bbd70f4 | ||
|
|
93044cc7d1 | ||
|
|
5a4eec5b08 | ||
|
|
e17b875641 | ||
|
|
a890d51bbc | ||
|
|
bb582416f2 | ||
|
|
b8526eca67 | ||
|
|
9c45746bd2 | ||
|
|
c4971e48c4 | ||
|
|
de6582b38b | ||
|
|
fc53abe589 | ||
|
|
7b23bf7c1b | ||
|
|
c0d3dd51b1 | ||
|
|
a96f3d153b | ||
|
|
31f3a508dc | ||
|
|
3b7c347b2e | ||
|
|
2e09d7d835 | ||
|
|
29cebd73e5 | ||
|
|
e4286ae7a3 | ||
|
|
c3f393bcc1 | ||
|
|
9aa54fd71b | ||
|
|
e85b953087 | ||
|
|
b776ba6b76 | ||
|
|
224b2c37d7 | ||
|
|
16a8f5a9c3 | ||
|
|
16fad51b5e | ||
|
|
287511c9b1 | ||
|
|
0a678eeacc | ||
|
|
c031139b89 | ||
|
|
710dc4fa94 | ||
|
|
ec53a7962e | ||
|
|
6f7d710129 | ||
|
|
513a8a3d26 | ||
|
|
c41c9a366f | ||
|
|
4385f03053 | ||
|
|
8e3b459d77 | ||
|
|
3807523f49 | ||
|
|
09997bb6c8 | ||
|
|
aa17729008 | ||
|
|
b59f3e6811 | ||
|
|
8427f40e8d | ||
|
|
e9c6a4a2d4 | ||
|
|
fb007d6bab | ||
|
|
4ca088ed12 | ||
|
|
ae2693425e | ||
|
|
d9b9485019 | ||
|
|
366da595af | ||
|
|
fb3d8e83c5 | ||
|
|
d14735ef4b | ||
|
|
3435327bc0 | ||
|
|
8a043edfd5 | ||
|
|
de07cf26e8 | ||
|
|
c737776958 | ||
|
|
7b0ad87781 | ||
|
|
3b92d5c1c6 | ||
|
|
cf1fc02d27 | ||
|
|
ba2e35e29c | ||
|
|
9afc067152 | ||
|
|
9fc182baf2 | ||
|
|
c2844697f3 | ||
|
|
fc0210c2fd | ||
|
|
f1df6f2d18 | ||
|
|
c3415b79fe | ||
|
|
af1e2887bd | ||
|
|
65e267ed3a | ||
|
|
6d574549bc | ||
|
|
f7c5b62ba3 | ||
|
|
8c230fee62 |
45
.github/workflows/daily-pr-recap.yml
vendored
45
.github/workflows/daily-pr-recap.yml
vendored
@@ -47,16 +47,15 @@ jobs:
|
||||
TODAY'S DATE: ${TODAY}
|
||||
|
||||
STEP 1: Gather PR data
|
||||
Run these commands to gather PR information:
|
||||
Run these commands to gather PR information. ONLY include PRs created or updated TODAY (${TODAY}):
|
||||
|
||||
# Open PRs with bug fix labels or 'fix' in title
|
||||
gh pr list --repo ${{ github.repository }} --state open --search \"fix in:title\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||
# PRs created today
|
||||
gh pr list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||
|
||||
# PRs with activity today (updated today)
|
||||
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||
|
||||
# PRs with high activity (get comments separately to filter bots)
|
||||
gh pr list --repo ${{ github.repository }} --state open --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft --limit 100
|
||||
|
||||
# Recently merged bug fixes
|
||||
gh pr list --repo ${{ github.repository }} --state merged --search \"merged:${TODAY} fix in:title\" --json number,title,author,mergedAt --limit 50
|
||||
|
||||
STEP 2: For high-activity PRs, check comment counts
|
||||
For promising PRs, run:
|
||||
@@ -66,21 +65,20 @@ jobs:
|
||||
- copilot-pull-request-reviewer
|
||||
- github-actions
|
||||
|
||||
STEP 3: Identify what matters
|
||||
STEP 3: Identify what matters (ONLY from today's PRs)
|
||||
|
||||
**Bug Fixes We Might Miss:**
|
||||
- PRs with 'fix' or 'bug' in title that have been open 2+ days
|
||||
**Bug Fixes From Today:**
|
||||
- PRs with 'fix' or 'bug' in title created/updated today
|
||||
- Small bug fixes (< 100 lines changed) that are easy to review
|
||||
- Bug fixes from community contributors (not core team)
|
||||
- Bug fixes from community contributors
|
||||
|
||||
**High Activity PRs:**
|
||||
- PRs with 5+ human comments (excluding bots listed above)
|
||||
- PRs with back-and-forth discussion
|
||||
- Controversial or complex changes getting attention
|
||||
**High Activity Today:**
|
||||
- PRs with significant human comments today (excluding bots listed above)
|
||||
- PRs with back-and-forth discussion today
|
||||
|
||||
**Quick Wins:**
|
||||
- Small PRs (< 50 lines) that are approved or nearly approved
|
||||
- Bug fixes that just need a final review
|
||||
- PRs that just need a final review
|
||||
|
||||
STEP 4: Generate the recap
|
||||
Create a structured recap:
|
||||
@@ -88,17 +86,14 @@ jobs:
|
||||
===DISCORD_START===
|
||||
**Daily PR Recap - ${TODAY}**
|
||||
|
||||
**Bug Fixes Needing Attention**
|
||||
[PRs fixing bugs that might be overlooked - prioritize by age and size]
|
||||
**New PRs Today**
|
||||
[PRs opened today - group by type: bug fixes, features, etc.]
|
||||
|
||||
**High Activity** (5+ human comments)
|
||||
[PRs with significant discussion - exclude bot comments]
|
||||
**Active PRs Today**
|
||||
[PRs with activity/updates today - significant discussion]
|
||||
|
||||
**Quick Wins** (small, ready to merge)
|
||||
[Easy PRs that just need a review/merge]
|
||||
|
||||
**Merged Bug Fixes Today**
|
||||
[What bug fixes shipped]
|
||||
**Quick Wins**
|
||||
[Small PRs ready to merge]
|
||||
===DISCORD_END===
|
||||
|
||||
STEP 5: Format for Discord
|
||||
|
||||
11
.github/workflows/test.yml
vendored
11
.github/workflows/test.yml
vendored
@@ -134,3 +134,14 @@ jobs:
|
||||
VITE_OPENCODE_SERVER_PORT: "4096"
|
||||
OPENCODE_CLIENT: "app"
|
||||
timeout-minutes: 30
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }}
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
path: |
|
||||
packages/app/e2e/test-results
|
||||
packages/app/e2e/playwright-report
|
||||
|
||||
3
.opencode/.gitignore
vendored
Normal file
3
.opencode/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
plans/
|
||||
bun.lock
|
||||
package.json
|
||||
@@ -71,15 +71,50 @@ Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
|
||||
- `packages/desktop`: The native desktop app, built with Tauri (wraps `packages/app`)
|
||||
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
||||
|
||||
### Understanding bun dev vs opencode
|
||||
|
||||
During development, `bun dev` is the local equivalent of the built `opencode` command. Both run the same CLI interface:
|
||||
|
||||
```bash
|
||||
# Development (from project root)
|
||||
bun dev --help # Show all available commands
|
||||
bun dev serve # Start headless API server
|
||||
bun dev web # Start server + open web interface
|
||||
bun dev <directory> # Start TUI in specific directory
|
||||
|
||||
# Production
|
||||
opencode --help # Show all available commands
|
||||
opencode serve # Start headless API server
|
||||
opencode web # Start server + open web interface
|
||||
opencode <directory> # Start TUI in specific directory
|
||||
```
|
||||
|
||||
### Running the API Server
|
||||
|
||||
To start the OpenCode headless API server:
|
||||
|
||||
```bash
|
||||
bun dev serve
|
||||
```
|
||||
|
||||
This starts the headless server on port 4096 by default. You can specify a different port:
|
||||
|
||||
```bash
|
||||
bun dev serve --port 8080
|
||||
```
|
||||
|
||||
### Running the Web App
|
||||
|
||||
To test UI changes during development, run the web app:
|
||||
To test UI changes during development:
|
||||
|
||||
1. **First, start the OpenCode server** (see [Running the API Server](#running-the-api-server) section above)
|
||||
2. **Then run the web app:**
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/app dev
|
||||
```
|
||||
|
||||
This starts a local dev server at http://localhost:5173 (or similar port shown in output). Most UI changes can be tested here.
|
||||
This starts a local dev server at http://localhost:5173 (or similar port shown in output). Most UI changes can be tested here, but the server must be running for full functionality.
|
||||
|
||||
### Running the Desktop App
|
||||
|
||||
@@ -127,9 +162,9 @@ Caveats:
|
||||
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
|
||||
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
|
||||
- If `spawn` does not work for you, you can debug the server separately:
|
||||
- Debug server: `bun run --inspect=ws://localhost:6499/ ./src/index.ts serve --port 4096`,
|
||||
- Debug server: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts serve --port 4096`,
|
||||
then attach TUI with `opencode attach http://localhost:4096`
|
||||
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --conditions=browser ./src/index.ts`
|
||||
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode --conditions=browser ./src/index.ts`
|
||||
|
||||
Other tips and tricks:
|
||||
|
||||
|
||||
2
STATS.md
2
STATS.md
@@ -207,3 +207,5 @@
|
||||
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
|
||||
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
|
||||
| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) |
|
||||
| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) |
|
||||
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
|
||||
|
||||
153
bun.lock
153
bun.lock
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -107,7 +107,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -182,7 +182,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -211,7 +211,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -240,7 +240,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -256,7 +256,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -284,7 +284,7 @@
|
||||
"@ai-sdk/vercel": "1.0.31",
|
||||
"@ai-sdk/xai": "2.0.51",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@gitlab/gitlab-ai-provider": "3.1.3",
|
||||
"@gitlab/gitlab-ai-provider": "3.2.0",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
@@ -311,6 +311,7 @@
|
||||
"clipboardy": "4.0.0",
|
||||
"decimal.js": "10.5.0",
|
||||
"diff": "catalog:",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gray-matter": "4.0.3",
|
||||
"hono": "catalog:",
|
||||
@@ -352,6 +353,8 @@
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"drizzle-kit": "0.31.8",
|
||||
"typescript": "catalog:",
|
||||
"vscode-languageserver-types": "3.17.5",
|
||||
"why-is-node-running": "3.2.2",
|
||||
@@ -360,7 +363,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -380,7 +383,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.4",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -391,7 +394,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -404,7 +407,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -445,7 +448,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -456,7 +459,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -922,7 +925,7 @@
|
||||
|
||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.3", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-ikumi4PZN/S0f+j/5rb5dBRtORyT41Pl/tj8vHhnpFtpYcxXsaNv2RvCKBVf2/PovvSz2pYMOcpujIU4MdGfyQ=="],
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.2.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-sqP34jDSWWEHygmYbM2rzIcRjhA+1FHVHj8mxUvVz1s7o2Cgb1NnOaUXU7eWTI0AGhO+tPYHDTqI/mRC4cdjlQ=="],
|
||||
|
||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||
|
||||
@@ -2042,12 +2045,18 @@
|
||||
|
||||
"before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
|
||||
|
||||
"better-sqlite3": ["better-sqlite3@12.6.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-FXI191x+D6UPWSze5IzZjhz+i9MK9nsuHsmTX9bXVl52k06AfZ2xql0lrgIUuzsMsJ7Vgl5kIptvDgBLIV3ZSQ=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
|
||||
"blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="],
|
||||
@@ -2254,6 +2263,10 @@
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
|
||||
@@ -2346,6 +2359,8 @@
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="],
|
||||
|
||||
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||
@@ -2430,6 +2445,8 @@
|
||||
|
||||
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
||||
|
||||
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
|
||||
|
||||
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||
|
||||
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
|
||||
@@ -2464,6 +2481,8 @@
|
||||
|
||||
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="],
|
||||
@@ -2502,6 +2521,8 @@
|
||||
|
||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
@@ -2552,6 +2573,8 @@
|
||||
|
||||
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||
|
||||
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"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=="],
|
||||
@@ -3090,6 +3113,8 @@
|
||||
|
||||
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
|
||||
|
||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||
|
||||
"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=="],
|
||||
@@ -3102,6 +3127,8 @@
|
||||
|
||||
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||
|
||||
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
@@ -3120,6 +3147,8 @@
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
|
||||
@@ -3132,6 +3161,8 @@
|
||||
|
||||
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
|
||||
|
||||
"node-abi": ["node-abi@3.85.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
@@ -3328,6 +3359,8 @@
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
|
||||
"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=="],
|
||||
@@ -3350,6 +3383,8 @@
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||
|
||||
"punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="],
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
@@ -3366,6 +3401,8 @@
|
||||
|
||||
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
|
||||
|
||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||
|
||||
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
||||
|
||||
"react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="],
|
||||
@@ -3552,6 +3589,10 @@
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
|
||||
|
||||
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
|
||||
|
||||
"simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="],
|
||||
@@ -3656,6 +3697,8 @@
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"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=="],
|
||||
@@ -3682,6 +3725,8 @@
|
||||
|
||||
"tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="],
|
||||
|
||||
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
|
||||
"terracotta": ["terracotta@1.0.6", "", { "dependencies": { "solid-use": "^0.9.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-yVrmT/Lg6a3tEbeYEJH8ksb1PYkR5FA9k5gr1TchaSNIiA2ZWs5a+koEbePXwlBP0poaV7xViZ/v50bQFcMgqw=="],
|
||||
@@ -3746,6 +3791,8 @@
|
||||
|
||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
"turbo": ["turbo@2.5.6", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.6", "turbo-darwin-arm64": "2.5.6", "turbo-linux-64": "2.5.6", "turbo-linux-arm64": "2.5.6", "turbo-windows-64": "2.5.6", "turbo-windows-arm64": "2.5.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w=="],
|
||||
|
||||
"turbo-darwin-64": ["turbo-darwin-64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A=="],
|
||||
@@ -4080,6 +4127,8 @@
|
||||
|
||||
"@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
@@ -4308,6 +4357,10 @@
|
||||
|
||||
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||
|
||||
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
@@ -4412,6 +4465,10 @@
|
||||
|
||||
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
|
||||
|
||||
"opencode/drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="],
|
||||
|
||||
"opencode/drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
||||
|
||||
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
||||
@@ -4442,6 +4499,8 @@
|
||||
|
||||
"postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"prebuild-install/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"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=="],
|
||||
@@ -4490,6 +4549,10 @@
|
||||
|
||||
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||
|
||||
"tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@@ -4944,6 +5007,8 @@
|
||||
|
||||
"babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
@@ -5018,6 +5083,8 @@
|
||||
|
||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.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-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
||||
@@ -5044,6 +5111,8 @@
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"tw-to-css/tailwindcss/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=="],
|
||||
|
||||
"tw-to-css/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
@@ -5190,6 +5259,56 @@
|
||||
|
||||
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"opencode/drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1768302833,
|
||||
"narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
|
||||
"lastModified": 1768393167,
|
||||
"narHash": "sha256-n2063BRjHde6DqAz2zavhOOiLUwA3qXt7jQYHyETjX8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "61db79b0c6b838d9894923920b612048e1201926",
|
||||
"rev": "2f594d5af95d4fdac67fba60376ec11e482041cb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -101,15 +101,26 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
|
||||
const zenProduct = new stripe.Product("ZenBlack", {
|
||||
name: "OpenCode Black",
|
||||
})
|
||||
const zenPrice = new stripe.Price("ZenBlackPrice", {
|
||||
const zenPriceProps = {
|
||||
product: zenProduct.id,
|
||||
unitAmount: 20000,
|
||||
currency: "usd",
|
||||
recurring: {
|
||||
interval: "month",
|
||||
intervalCount: 1,
|
||||
},
|
||||
}
|
||||
const zenPrice200 = new stripe.Price("ZenBlackPrice", { ...zenPriceProps, unitAmount: 20000 })
|
||||
const zenPrice100 = new stripe.Price("ZenBlack100Price", { ...zenPriceProps, unitAmount: 10000 })
|
||||
const zenPrice20 = new stripe.Price("ZenBlack20Price", { ...zenPriceProps, unitAmount: 2000 })
|
||||
const ZEN_BLACK_PRICE = new sst.Linkable("ZEN_BLACK_PRICE", {
|
||||
properties: {
|
||||
product: zenProduct.id,
|
||||
plan200: zenPrice200.id,
|
||||
plan100: zenPrice100.id,
|
||||
plan20: zenPrice20.id,
|
||||
},
|
||||
})
|
||||
const ZEN_BLACK_LIMITS = new sst.Secret("ZEN_BLACK_LIMITS")
|
||||
|
||||
const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS1"),
|
||||
@@ -121,7 +132,6 @@ const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS7"),
|
||||
new sst.Secret("ZEN_MODELS8"),
|
||||
]
|
||||
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
@@ -164,7 +174,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
EMAILOCTOPUS_API_KEY,
|
||||
AWS_SES_ACCESS_KEY_ID,
|
||||
AWS_SES_SECRET_ACCESS_KEY,
|
||||
ZEN_BLACK,
|
||||
ZEN_BLACK_PRICE,
|
||||
ZEN_BLACK_LIMITS,
|
||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||
...ZEN_MODELS,
|
||||
...($dev
|
||||
|
||||
@@ -6,7 +6,7 @@ export const domain = (() => {
|
||||
|
||||
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
|
||||
|
||||
new cloudflare.RegionalHostname("RegionalHostname", {
|
||||
new cloudflxare.RegionalHostname("RegionalHostname", {
|
||||
hostname: domain,
|
||||
regionKey: "us",
|
||||
zoneId: zoneID,
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-sH6zUk9G4vC6btPZIR9aiSHX0F4aGyUZB7fKbpDUcpE=",
|
||||
"aarch64-linux": "sha256-CVpdXnFns34hmGwwlRrrI6Uk6B/jZUxfnH4HC2NanEo=",
|
||||
"aarch64-darwin": "sha256-khP27Iiq+FAZRlzUy7rGXc2MviZjirFH1ShRyd7q1bY=",
|
||||
"x86_64-darwin": "sha256-nE2p62Tld64sQVMq7j0YNT5Zwjqp22H997+K8xfi1ag="
|
||||
<<<<<<< HEAD
|
||||
"x86_64-linux": "sha256-H8QVUC5shGI97Ut/wDSYsSuprHpwssJ1MHSHojn+zNI=",
|
||||
"aarch64-linux": "sha256-4BlpH/oIXRJEjkQydXDv1oi1Yx7li3k1dKHUy2/Gb10=",
|
||||
"aarch64-darwin": "sha256-IOgZ/LP4lvFX3OlalaFuQFYAEFwP+lxz3BRwvu4Hmj4=",
|
||||
"x86_64-darwin": "sha256-CHrE2z+LqY2WXTQeGWG5LNMF1AY4UGSwViJAy4IwIVw="
|
||||
=======
|
||||
"x86_64-linux": "sha256-9QHW6Ue9VO1VKsu6sg4gRtxgifQGNJlfVVXaa0Uc0XQ=",
|
||||
<<<<<<< HEAD
|
||||
"aarch64-darwin": "sha256-IOgZ/LP4lvFX3OlalaFuQFYAEFwP+lxz3BRwvu4Hmj4="
|
||||
>>>>>>> 6e0a58c50 (Update Nix flake.lock and x86_64-linux hash)
|
||||
=======
|
||||
"aarch64-darwin": "sha256-G8tTkuUSFQNOmjbu6cIi6qeyNWtGogtUVNi2CSgcgX0="
|
||||
>>>>>>> 8a0e3e909 (Update aarch64-darwin hash)
|
||||
}
|
||||
}
|
||||
|
||||
35
packages/app/e2e/file-viewer.spec.ts
Normal file
35
packages/app/e2e/file-viewer.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { modKey } from "./utils"
|
||||
|
||||
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const sep = process.platform === "win32" ? "\\" : "/"
|
||||
const file = ["packages", "app", "package.json"].join(sep)
|
||||
|
||||
await page.keyboard.press(`${modKey}+P`)
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const input = dialog.getByRole("textbox").first()
|
||||
await input.fill(file)
|
||||
|
||||
const fileItem = dialog
|
||||
.locator(
|
||||
'[data-slot="list-item"][data-key^="file:"][data-key*="packages"][data-key*="app"][data-key$="package.json"]',
|
||||
)
|
||||
.first()
|
||||
await expect(fileItem).toBeVisible()
|
||||
await fileItem.click()
|
||||
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
const tab = page.getByRole("tab", { name: "package.json" })
|
||||
await expect(tab).toBeVisible()
|
||||
await tab.click()
|
||||
|
||||
const code = page.locator('[data-component="code"]').first()
|
||||
await expect(code).toBeVisible()
|
||||
await expect(code.getByText("@opencode-ai/app")).toBeVisible()
|
||||
})
|
||||
43
packages/app/e2e/model-picker.spec.ts
Normal file
43
packages/app/e2e/model-picker.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { promptSelector } from "./utils"
|
||||
|
||||
test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type("/model")
|
||||
|
||||
const command = page.locator('[data-slash-id="model.choose"]')
|
||||
await expect(command).toBeVisible()
|
||||
await command.hover()
|
||||
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const input = dialog.getByRole("textbox").first()
|
||||
|
||||
const selected = dialog.locator('[data-slot="list-item"][data-selected="true"]').first()
|
||||
await expect(selected).toBeVisible()
|
||||
|
||||
const other = dialog.locator('[data-slot="list-item"]:not([data-selected="true"])').first()
|
||||
const target = (await other.count()) > 0 ? other : selected
|
||||
|
||||
const key = await target.getAttribute("data-key")
|
||||
if (!key) throw new Error("Failed to resolve model key from list item")
|
||||
|
||||
const name = (await target.locator("span").first().innerText()).trim()
|
||||
const model = key.split(":").slice(1).join(":")
|
||||
|
||||
await input.fill(model)
|
||||
|
||||
const item = dialog.locator(`[data-slot="list-item"][data-key="${key}"]`)
|
||||
await expect(item).toBeVisible()
|
||||
await item.click()
|
||||
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
const form = page.locator(promptSelector).locator("xpath=ancestor::form[1]")
|
||||
await expect(form.locator('[data-component="button"]').filter({ hasText: name }).first()).toBeVisible()
|
||||
})
|
||||
26
packages/app/e2e/prompt-mention.spec.ts
Normal file
26
packages/app/e2e/prompt-mention.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { promptSelector } from "./utils"
|
||||
|
||||
test("smoke @mention inserts file pill token", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
const sep = process.platform === "win32" ? "\\" : "/"
|
||||
const file = ["packages", "app", "package.json"].join(sep)
|
||||
const filePattern = /packages[\\/]+app[\\/]+\s*package\.json/
|
||||
|
||||
await page.keyboard.type(`@${file}`)
|
||||
|
||||
const suggestion = page.getByRole("button", { name: filePattern }).first()
|
||||
await expect(suggestion).toBeVisible()
|
||||
await suggestion.hover()
|
||||
|
||||
await page.keyboard.press("Tab")
|
||||
|
||||
const pill = page.locator(`${promptSelector} [data-type="file"]`).first()
|
||||
await expect(pill).toBeVisible()
|
||||
await expect(pill).toHaveAttribute("data-path", filePattern)
|
||||
|
||||
await page.keyboard.type(" ok")
|
||||
await expect(page.locator(promptSelector)).toContainText("ok")
|
||||
})
|
||||
22
packages/app/e2e/prompt-slash-open.spec.ts
Normal file
22
packages/app/e2e/prompt-slash-open.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { promptSelector } from "./utils"
|
||||
|
||||
test("smoke /open opens file picker dialog", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type("/open")
|
||||
|
||||
const command = page.locator('[data-slash-id="file.open"]')
|
||||
await expect(command).toBeVisible()
|
||||
await command.hover()
|
||||
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByRole("textbox").first()).toBeVisible()
|
||||
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(dialog).toHaveCount(0)
|
||||
})
|
||||
44
packages/app/e2e/settings.spec.ts
Normal file
44
packages/app/e2e/settings.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { modKey } from "./utils"
|
||||
|
||||
test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
|
||||
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
|
||||
|
||||
const opened = await dialog
|
||||
.waitFor({ state: "visible", timeout: 3000 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (!opened) {
|
||||
await page.getByRole("button", { name: "Settings" }).first().click()
|
||||
await expect(dialog).toBeVisible()
|
||||
}
|
||||
|
||||
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||
await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible()
|
||||
await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible()
|
||||
|
||||
await page.keyboard.press("Escape")
|
||||
|
||||
const closed = await dialog
|
||||
.waitFor({ state: "detached", timeout: 1500 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (closed) return
|
||||
|
||||
await page.keyboard.press("Escape")
|
||||
const closedSecond = await dialog
|
||||
.waitFor({ state: "detached", timeout: 1500 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (closedSecond) return
|
||||
|
||||
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
|
||||
await expect(dialog).toHaveCount(0)
|
||||
})
|
||||
25
packages/app/e2e/terminal-init.spec.ts
Normal file
25
packages/app/e2e/terminal-init.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { promptSelector, terminalSelector, terminalToggleKey } from "./utils"
|
||||
|
||||
test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const terminals = page.locator(terminalSelector)
|
||||
const opened = await terminals.first().isVisible()
|
||||
|
||||
if (!opened) {
|
||||
await page.keyboard.press(terminalToggleKey)
|
||||
}
|
||||
|
||||
await expect(terminals.first()).toBeVisible()
|
||||
await expect(terminals.first().locator("textarea")).toHaveCount(1)
|
||||
await expect(terminals).toHaveCount(1)
|
||||
|
||||
// Ghostty captures a lot of keybinds when focused; move focus back
|
||||
// to the app shell before triggering `terminal.new`.
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.press("Control+Alt+T")
|
||||
|
||||
await expect(terminals).toHaveCount(2)
|
||||
await expect(terminals.nth(1).locator("textarea")).toHaveCount(1)
|
||||
})
|
||||
8
packages/app/e2e/tsconfig.json
Normal file
8
packages/app/e2e/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -19,10 +19,12 @@ import { SettingsProvider } from "@/context/settings"
|
||||
import { TerminalProvider } from "@/context/terminal"
|
||||
import { PromptProvider } from "@/context/prompt"
|
||||
import { FileProvider } from "@/context/file"
|
||||
import { CommentsProvider } from "@/context/comments"
|
||||
import { NotificationProvider } from "@/context/notification"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { CommandProvider } from "@/context/command"
|
||||
import { LanguageProvider, useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import Layout from "@/pages/layout"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
@@ -45,6 +47,11 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
function MarkedProviderWithNativeParser(props: ParentProps) {
|
||||
const platform = usePlatform()
|
||||
return <MarkedProvider nativeParser={platform.parseMarkdown}>{props.children}</MarkedProvider>
|
||||
}
|
||||
|
||||
export function AppBaseProviders(props: ParentProps) {
|
||||
return (
|
||||
<MetaProvider>
|
||||
@@ -54,11 +61,11 @@ export function AppBaseProviders(props: ParentProps) {
|
||||
<UiI18nBridge>
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<MarkedProviderWithNativeParser>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
</MarkedProviderWithNativeParser>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
</UiI18nBridge>
|
||||
@@ -120,13 +127,15 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id ?? "new"} keyed>
|
||||
<Show when={p.params.id ?? "new"}>
|
||||
<TerminalProvider>
|
||||
<FileProvider>
|
||||
<PromptProvider>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Session />
|
||||
</Suspense>
|
||||
<CommentsProvider>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Session />
|
||||
</Suspense>
|
||||
</CommentsProvider>
|
||||
</PromptProvider>
|
||||
</FileProvider>
|
||||
</TerminalProvider>
|
||||
|
||||
@@ -143,7 +143,17 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} />}>
|
||||
<Dialog
|
||||
title={
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
icon="arrow-left"
|
||||
variant="ghost"
|
||||
onClick={goBack}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
||||
<div class="px-2.5 flex gap-4 items-center">
|
||||
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
@@ -177,7 +187,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2">
|
||||
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||
<div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||
</div>
|
||||
<span>{methodLabel(i)}</span>
|
||||
</div>
|
||||
@@ -363,6 +373,9 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
if (store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
}
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
@@ -16,6 +17,7 @@ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] a
|
||||
export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const dialog = useDialog()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const language = useLanguage()
|
||||
|
||||
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||
@@ -25,6 +27,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
name: defaultName(),
|
||||
color: props.project.icon?.color || "pink",
|
||||
iconUrl: props.project.icon?.override || "",
|
||||
startup: props.project.commands?.start ?? "",
|
||||
saving: false,
|
||||
})
|
||||
|
||||
@@ -69,15 +72,29 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
if (!props.project.id) return
|
||||
|
||||
setStore("saving", true)
|
||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||
await globalSDK.client.project.update({
|
||||
projectID: props.project.id,
|
||||
directory: props.project.worktree,
|
||||
const start = store.startup.trim()
|
||||
|
||||
if (props.project.id && props.project.id !== "global") {
|
||||
await globalSDK.client.project.update({
|
||||
projectID: props.project.id,
|
||||
directory: props.project.worktree,
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl },
|
||||
commands: { start },
|
||||
})
|
||||
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
||||
setStore("saving", false)
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
|
||||
globalSync.project.meta(props.project.worktree, {
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl },
|
||||
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||
commands: { start: start || undefined },
|
||||
})
|
||||
setStore("saving", false)
|
||||
dialog.close()
|
||||
@@ -193,6 +210,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
{(color) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={language.t("dialog.project.edit.color.select", { color })}
|
||||
aria-pressed={store.color === color}
|
||||
classList={{
|
||||
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
||||
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
|
||||
@@ -213,6 +232,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<TextField
|
||||
multiline
|
||||
label={language.t("dialog.project.edit.worktree.startup")}
|
||||
description={language.t("dialog.project.edit.worktree.startup.description")}
|
||||
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
|
||||
value={store.startup}
|
||||
onChange={(v) => setStore("startup", v)}
|
||||
spellcheck={false}
|
||||
class="max-h-40 w-full font-mono text-xs no-scrollbar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
@@ -21,63 +22,153 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const language = useLanguage()
|
||||
|
||||
const home = createMemo(() => sync.data.path.home)
|
||||
const root = createMemo(() => sync.data.path.home || sync.data.path.directory)
|
||||
|
||||
const start = createMemo(() => sync.data.path.home || sync.data.path.directory)
|
||||
|
||||
const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
|
||||
|
||||
function normalize(input: string) {
|
||||
const v = input.replaceAll("\\", "/")
|
||||
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
|
||||
return v.replace(/\/+/g, "/")
|
||||
}
|
||||
|
||||
function normalizeDriveRoot(input: string) {
|
||||
const v = normalize(input)
|
||||
if (/^[A-Za-z]:$/.test(v)) return v + "/"
|
||||
return v
|
||||
}
|
||||
|
||||
function trimTrailing(input: string) {
|
||||
const v = normalizeDriveRoot(input)
|
||||
if (v === "/") return v
|
||||
if (v === "//") return v
|
||||
if (/^[A-Za-z]:\/$/.test(v)) return v
|
||||
return v.replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
function join(base: string | undefined, rel: string) {
|
||||
const b = (base ?? "").replace(/[\\/]+$/, "")
|
||||
const r = rel.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")
|
||||
const b = trimTrailing(base ?? "")
|
||||
const r = trimTrailing(rel).replace(/^\/+/, "")
|
||||
if (!b) return r
|
||||
if (!r) return b
|
||||
if (b.endsWith("/")) return b + r
|
||||
return b + "/" + r
|
||||
}
|
||||
|
||||
function display(rel: string) {
|
||||
const full = join(root(), rel)
|
||||
function rootOf(input: string) {
|
||||
const v = normalizeDriveRoot(input)
|
||||
if (v.startsWith("//")) return "//"
|
||||
if (v.startsWith("/")) return "/"
|
||||
if (/^[A-Za-z]:\//.test(v)) return v.slice(0, 3)
|
||||
return ""
|
||||
}
|
||||
|
||||
function display(path: string) {
|
||||
const full = trimTrailing(path)
|
||||
const h = home()
|
||||
if (!h) return full
|
||||
if (full === h) return "~"
|
||||
if (full.startsWith(h + "/") || full.startsWith(h + "\\")) {
|
||||
return "~" + full.slice(h.length)
|
||||
}
|
||||
|
||||
const hn = trimTrailing(h)
|
||||
const lc = full.toLowerCase()
|
||||
const hc = hn.toLowerCase()
|
||||
if (lc === hc) return "~"
|
||||
if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
|
||||
return full
|
||||
}
|
||||
|
||||
function normalizeQuery(query: string) {
|
||||
function scoped(filter: string) {
|
||||
const base = start()
|
||||
if (!base) return
|
||||
|
||||
const raw = normalizeDriveRoot(filter.trim())
|
||||
if (!raw) return { directory: trimTrailing(base), path: "" }
|
||||
|
||||
const h = home()
|
||||
if (raw === "~") return { directory: trimTrailing(h ?? base), path: "" }
|
||||
if (raw.startsWith("~/")) return { directory: trimTrailing(h ?? base), path: raw.slice(2) }
|
||||
|
||||
if (!query) return query
|
||||
if (query.startsWith("~/")) return query.slice(2)
|
||||
|
||||
if (h) {
|
||||
const lc = query.toLowerCase()
|
||||
const hc = h.toLowerCase()
|
||||
if (lc === hc || lc.startsWith(hc + "/") || lc.startsWith(hc + "\\")) {
|
||||
return query.slice(h.length).replace(/^[\\/]+/, "")
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
const root = rootOf(raw)
|
||||
if (root) return { directory: trimTrailing(root), path: raw.slice(root.length) }
|
||||
return { directory: trimTrailing(base), path: raw }
|
||||
}
|
||||
|
||||
async function fetchDirs(query: string) {
|
||||
const directory = root()
|
||||
if (!directory) return [] as string[]
|
||||
async function dirs(dir: string) {
|
||||
const key = trimTrailing(dir)
|
||||
const existing = cache.get(key)
|
||||
if (existing) return existing
|
||||
|
||||
const results = await sdk.client.find
|
||||
.files({ directory, query, type: "directory", limit: 50 })
|
||||
const request = sdk.client.file
|
||||
.list({ directory: key, path: "" })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [])
|
||||
.then((nodes) =>
|
||||
nodes
|
||||
.filter((n) => n.type === "directory")
|
||||
.map((n) => ({
|
||||
name: n.name,
|
||||
absolute: trimTrailing(normalizeDriveRoot(n.absolute)),
|
||||
})),
|
||||
)
|
||||
|
||||
return results.map((x) => x.replace(/[\\/]+$/, ""))
|
||||
cache.set(key, request)
|
||||
return request
|
||||
}
|
||||
|
||||
async function match(dir: string, query: string, limit: number) {
|
||||
const items = await dirs(dir)
|
||||
if (!query) return items.slice(0, limit).map((x) => x.absolute)
|
||||
return fuzzysort.go(query, items, { key: "name", limit }).map((x) => x.obj.absolute)
|
||||
}
|
||||
|
||||
const directories = async (filter: string) => {
|
||||
const query = normalizeQuery(filter.trim())
|
||||
return fetchDirs(query)
|
||||
const input = scoped(filter)
|
||||
if (!input) return [] as string[]
|
||||
|
||||
const raw = normalizeDriveRoot(filter.trim())
|
||||
const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
|
||||
|
||||
const query = normalizeDriveRoot(input.path)
|
||||
|
||||
if (!isPath) {
|
||||
const results = await sdk.client.find
|
||||
.files({ directory: input.directory, query, type: "directory", limit: 50 })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [])
|
||||
|
||||
return results.map((rel) => join(input.directory, rel)).slice(0, 50)
|
||||
}
|
||||
|
||||
const segments = query.replace(/^\/+/, "").split("/")
|
||||
const head = segments.slice(0, segments.length - 1).filter((x) => x && x !== ".")
|
||||
const tail = segments[segments.length - 1] ?? ""
|
||||
|
||||
const cap = 12
|
||||
const branch = 4
|
||||
let paths = [input.directory]
|
||||
for (const part of head) {
|
||||
if (part === "..") {
|
||||
paths = paths.map((p) => {
|
||||
const v = trimTrailing(p)
|
||||
if (v === "/") return v
|
||||
if (/^[A-Za-z]:\/$/.test(v)) return v
|
||||
const i = v.lastIndexOf("/")
|
||||
if (i <= 0) return "/"
|
||||
return v.slice(0, i)
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const next = (await Promise.all(paths.map((p) => match(p, part, branch)))).flat()
|
||||
paths = Array.from(new Set(next)).slice(0, cap)
|
||||
if (paths.length === 0) return [] as string[]
|
||||
}
|
||||
|
||||
const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
|
||||
return Array.from(new Set(out)).slice(0, 50)
|
||||
}
|
||||
|
||||
function resolve(rel: string) {
|
||||
const absolute = join(root(), rel)
|
||||
function resolve(absolute: string) {
|
||||
props.onSelect(props.multiple ? [absolute] : absolute)
|
||||
dialog.close()
|
||||
}
|
||||
@@ -95,12 +186,12 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
resolve(path)
|
||||
}}
|
||||
>
|
||||
{(rel) => {
|
||||
const path = display(rel)
|
||||
{(absolute) => {
|
||||
const path = display(absolute)
|
||||
return (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: rel, type: "directory" }} class="shrink-0 size-4" />
|
||||
<FileIcon node={{ path: absolute, type: "directory" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(path)}
|
||||
|
||||
@@ -32,8 +32,8 @@ export function DialogSelectFile() {
|
||||
const dialog = useDialog()
|
||||
const params = useParams()
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const [grouped, setGrouped] = createSignal(false)
|
||||
const common = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
||||
import { Component, createMemo, createSignal, JSX, Show } from "solid-js"
|
||||
import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
@@ -86,10 +86,12 @@ const ModelList: Component<{
|
||||
)
|
||||
}
|
||||
|
||||
export const ModelSelectorPopover: Component<{
|
||||
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||
provider?: string
|
||||
children: JSX.Element
|
||||
}> = (props) => {
|
||||
children?: JSX.Element
|
||||
triggerAs?: T
|
||||
triggerProps?: ComponentProps<T>
|
||||
}) {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const dialog = useDialog()
|
||||
|
||||
@@ -101,7 +103,9 @@ export const ModelSelectorPopover: Component<{
|
||||
|
||||
return (
|
||||
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
|
||||
<Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
|
||||
<Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}>
|
||||
{props.children}
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
|
||||
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
|
||||
|
||||
@@ -56,6 +56,12 @@ export const DialogSelectProvider: Component = () => {
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
||||
</Show>
|
||||
<Show when={i.id === "openai"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
|
||||
</Show>
|
||||
<Show when={i.id.startsWith("github-copilot")}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
||||
@@ -158,6 +158,7 @@ export function DialogSelectServer() {
|
||||
icon="circle-x"
|
||||
variant="ghost"
|
||||
class="bg-transparent transition-opacity shrink-0 hover:scale-110"
|
||||
aria-label={language.t("dialog.server.action.remove")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleRemove(i)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { SettingsGeneral } from "./settings-general"
|
||||
import { SettingsKeybinds } from "./settings-keybinds"
|
||||
import { SettingsPermissions } from "./settings-permissions"
|
||||
@@ -14,6 +15,7 @@ import { SettingsMcp } from "./settings-mcp"
|
||||
|
||||
export const DialogSettings: Component = () => {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
|
||||
return (
|
||||
<Dialog size="x-large">
|
||||
@@ -23,22 +25,35 @@ export const DialogSettings: Component = () => {
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "12px",
|
||||
"justify-content": "space-between",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
"padding-top": "12px",
|
||||
"padding-bottom": "12px",
|
||||
}}
|
||||
>
|
||||
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
|
||||
<Tabs.Trigger value="general">
|
||||
<Icon name="sliders" />
|
||||
{language.t("settings.tab.general")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="shortcuts">
|
||||
<Icon name="keyboard" />
|
||||
{language.t("settings.tab.shortcuts")}
|
||||
</Tabs.Trigger>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "12px",
|
||||
width: "100%",
|
||||
"padding-top": "12px",
|
||||
}}
|
||||
>
|
||||
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
|
||||
<Tabs.Trigger value="general">
|
||||
<Icon name="sliders" />
|
||||
{language.t("settings.tab.general")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="shortcuts">
|
||||
<Icon name="keyboard" />
|
||||
{language.t("settings.tab.shortcuts")}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
|
||||
<span>OpenCode Desktop</span>
|
||||
<span class="text-11-regular">v{platform.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <Tabs.SectionTitle>Server</Tabs.SectionTitle> */}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useLayout } from "@/context/layout"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
@@ -47,6 +48,7 @@ import { useProviders } from "@/hooks/use-providers"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -60,11 +62,19 @@ import { base64Encode } from "@opencode-ai/util/encode"
|
||||
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
||||
|
||||
type PendingPrompt = {
|
||||
abort: AbortController
|
||||
cleanup: VoidFunction
|
||||
}
|
||||
|
||||
const pending = new Map<string, PendingPrompt>()
|
||||
|
||||
interface PromptInputProps {
|
||||
class?: string
|
||||
ref?: (el: HTMLDivElement) => void
|
||||
newSessionWorktree?: string
|
||||
onNewSessionWorktreeReset?: () => void
|
||||
onSubmit?: () => void
|
||||
}
|
||||
|
||||
const EXAMPLES = [
|
||||
@@ -114,6 +124,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const files = useFile()
|
||||
const prompt = usePrompt()
|
||||
const layout = useLayout()
|
||||
const comments = useComments()
|
||||
const params = useParams()
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
@@ -156,11 +167,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const activeFile = createMemo(() => {
|
||||
const tab = tabs().active()
|
||||
if (!tab) return
|
||||
return files.pathFromTab(tab)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
|
||||
const recent = createMemo(() => {
|
||||
const all = tabs().all()
|
||||
const active = tabs().active()
|
||||
const order = active ? [active, ...all.filter((x) => x !== active)] : all
|
||||
const seen = new Set<string>()
|
||||
const paths: string[] = []
|
||||
|
||||
for (const tab of order) {
|
||||
const path = files.pathFromTab(tab)
|
||||
if (!path) continue
|
||||
if (seen.has(path)) continue
|
||||
seen.add(path)
|
||||
paths.push(path)
|
||||
}
|
||||
|
||||
return paths
|
||||
})
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const status = createMemo(
|
||||
@@ -381,7 +406,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (!isFocused()) setComposing(false)
|
||||
})
|
||||
|
||||
type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string }
|
||||
type AtOption =
|
||||
| { type: "agent"; name: string; display: string }
|
||||
| { type: "file"; path: string; display: string; recent?: boolean }
|
||||
|
||||
const agentList = createMemo(() =>
|
||||
sync.data.agent
|
||||
@@ -412,12 +439,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
} = useFilteredList<AtOption>({
|
||||
items: async (query) => {
|
||||
const agents = agentList()
|
||||
const open = recent()
|
||||
const seen = new Set(open)
|
||||
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
|
||||
const paths = await files.searchFilesAndDirectories(query)
|
||||
const fileOptions: AtOption[] = paths.map((path) => ({ type: "file", path, display: path }))
|
||||
return [...agents, ...fileOptions]
|
||||
const fileOptions: AtOption[] = paths
|
||||
.filter((path) => !seen.has(path))
|
||||
.map((path) => ({ type: "file", path, display: path }))
|
||||
return [...agents, ...pinned, ...fileOptions]
|
||||
},
|
||||
key: atKey,
|
||||
filterKeys: ["display"],
|
||||
groupBy: (item) => {
|
||||
if (item.type === "agent") return "agent"
|
||||
if (item.recent) return "recent"
|
||||
return "file"
|
||||
},
|
||||
sortGroupsBy: (a, b) => {
|
||||
const rank = (category: string) => {
|
||||
if (category === "agent") return 0
|
||||
if (category === "recent") return 1
|
||||
return 2
|
||||
}
|
||||
return rank(a.category) - rank(b.category)
|
||||
},
|
||||
onSelect: handleAtSelect,
|
||||
})
|
||||
|
||||
@@ -809,12 +854,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
setStore("popover", null)
|
||||
}
|
||||
|
||||
const abort = () =>
|
||||
sdk.client.session
|
||||
const abort = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return Promise.resolve()
|
||||
const queued = pending.get(sessionID)
|
||||
if (queued) {
|
||||
queued.abort.abort()
|
||||
queued.cleanup()
|
||||
pending.delete(sessionID)
|
||||
return Promise.resolve()
|
||||
}
|
||||
return sdk.client.session
|
||||
.abort({
|
||||
sessionID: params.id!,
|
||||
sessionID,
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
|
||||
const text = prompt
|
||||
@@ -1074,6 +1129,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
return
|
||||
}
|
||||
WorktreeState.pending(createdWorktree.directory)
|
||||
sessionDirectory = createdWorktree.directory
|
||||
}
|
||||
|
||||
@@ -1110,6 +1166,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
if (!session) return
|
||||
|
||||
props.onSubmit?.()
|
||||
|
||||
const model = {
|
||||
modelID: currentModel.id,
|
||||
providerID: currentModel.provider.id,
|
||||
@@ -1228,37 +1286,69 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
const usedUrls = new Set(fileAttachmentParts.map((part) => part.url))
|
||||
|
||||
const contextFileParts: Array<{
|
||||
id: string
|
||||
type: "file"
|
||||
mime: string
|
||||
url: string
|
||||
filename?: string
|
||||
}> = []
|
||||
const context = prompt.context.items().slice()
|
||||
|
||||
const addContextFile = (path: string, selection?: FileSelection) => {
|
||||
const absolute = toAbsolutePath(path)
|
||||
const query = selection ? `?start=${selection.startLine}&end=${selection.endLine}` : ""
|
||||
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
|
||||
|
||||
const contextParts: Array<
|
||||
| {
|
||||
id: string
|
||||
type: "text"
|
||||
text: string
|
||||
synthetic?: boolean
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "file"
|
||||
mime: string
|
||||
url: string
|
||||
filename?: string
|
||||
}
|
||||
> = []
|
||||
|
||||
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 addContextFile = (input: { path: string; selection?: FileSelection; comment?: string }) => {
|
||||
const absolute = toAbsolutePath(input.path)
|
||||
const query = input.selection ? `?start=${input.selection.startLine}&end=${input.selection.endLine}` : ""
|
||||
const url = `file://${absolute}${query}`
|
||||
if (usedUrls.has(url)) return
|
||||
|
||||
const comment = input.comment?.trim()
|
||||
if (!comment && usedUrls.has(url)) return
|
||||
usedUrls.add(url)
|
||||
contextFileParts.push({
|
||||
|
||||
if (comment) {
|
||||
contextParts.push({
|
||||
id: Identifier.ascending("part"),
|
||||
type: "text",
|
||||
text: commentNote(input.path, input.selection, comment),
|
||||
synthetic: true,
|
||||
})
|
||||
}
|
||||
|
||||
contextParts.push({
|
||||
id: Identifier.ascending("part"),
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url,
|
||||
filename: getFilename(path),
|
||||
filename: getFilename(input.path),
|
||||
})
|
||||
}
|
||||
|
||||
const activePath = activeFile()
|
||||
if (activePath && prompt.context.activeTab()) {
|
||||
addContextFile(activePath)
|
||||
}
|
||||
|
||||
for (const item of prompt.context.items()) {
|
||||
for (const item of context) {
|
||||
if (item.type !== "file") continue
|
||||
addContextFile(item.path, item.selection)
|
||||
addContextFile({ path: item.path, selection: item.selection, comment: item.comment })
|
||||
}
|
||||
|
||||
const imageAttachmentParts = images.map((attachment) => ({
|
||||
@@ -1278,7 +1368,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const requestParts = [
|
||||
textPart,
|
||||
...fileAttachmentParts,
|
||||
...contextFileParts,
|
||||
...contextParts,
|
||||
...agentAttachmentParts,
|
||||
...imageAttachmentParts,
|
||||
]
|
||||
@@ -1298,10 +1388,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
model,
|
||||
}
|
||||
|
||||
const setSyncStore = sessionDirectory === projectDirectory ? sync.set : globalSync.child(sessionDirectory)[1]
|
||||
|
||||
const addOptimisticMessage = () => {
|
||||
setSyncStore(
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session.id]
|
||||
if (!messages) {
|
||||
draft.message[session.id] = [optimisticMessage]
|
||||
} else {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
messages.splice(result.index, 0, optimisticMessage)
|
||||
}
|
||||
draft.part[messageID] = optimisticParts
|
||||
.filter((p) => !!p?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
globalSync.child(sessionDirectory)[1](
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session.id]
|
||||
if (!messages) {
|
||||
@@ -1319,7 +1426,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
|
||||
const removeOptimisticMessage = () => {
|
||||
setSyncStore(
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session.id]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
}
|
||||
delete draft.part[messageID]
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
globalSync.child(sessionDirectory)[1](
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session.id]
|
||||
if (messages) {
|
||||
@@ -1331,11 +1452,75 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
for (const item of commentItems) {
|
||||
prompt.context.remove(item.key)
|
||||
}
|
||||
|
||||
clearInput()
|
||||
addOptimisticMessage()
|
||||
|
||||
client.session
|
||||
.prompt({
|
||||
const waitForWorktree = async () => {
|
||||
const worktree = WorktreeState.get(sessionDirectory)
|
||||
if (!worktree || worktree.status !== "pending") return true
|
||||
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set("session_status", session.id, { type: "busy" })
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
|
||||
const cleanup = () => {
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set("session_status", session.id, { type: "idle" })
|
||||
}
|
||||
removeOptimisticMessage()
|
||||
for (const item of commentItems) {
|
||||
prompt.context.add({
|
||||
type: "file",
|
||||
path: item.path,
|
||||
selection: item.selection,
|
||||
comment: item.comment,
|
||||
commentID: item.commentID,
|
||||
preview: item.preview,
|
||||
})
|
||||
}
|
||||
restoreInput()
|
||||
}
|
||||
|
||||
pending.set(session.id, { abort: controller, cleanup })
|
||||
|
||||
const abort = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||
if (controller.signal.aborted) {
|
||||
resolve({ status: "failed", message: "aborted" })
|
||||
return
|
||||
}
|
||||
controller.signal.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
resolve({ status: "failed", message: "aborted" })
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
})
|
||||
|
||||
const timeoutMs = 5 * 60 * 1000
|
||||
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ status: "failed", message: "Workspace is still preparing" })
|
||||
}, timeoutMs)
|
||||
})
|
||||
|
||||
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout])
|
||||
pending.delete(session.id)
|
||||
if (controller.signal.aborted) return false
|
||||
if (result.status === "failed") throw new Error(result.message)
|
||||
return true
|
||||
}
|
||||
|
||||
const send = async () => {
|
||||
const ok = await waitForWorktree()
|
||||
if (!ok) return
|
||||
await client.session.prompt({
|
||||
sessionID: session.id,
|
||||
agent,
|
||||
model,
|
||||
@@ -1343,14 +1528,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
parts: requestParts,
|
||||
variant,
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
removeOptimisticMessage()
|
||||
restoreInput()
|
||||
}
|
||||
|
||||
void send().catch((err) => {
|
||||
pending.delete(session.id)
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set("session_status", session.id, { type: "idle" })
|
||||
}
|
||||
showToast({
|
||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
removeOptimisticMessage()
|
||||
for (const item of commentItems) {
|
||||
prompt.context.add({
|
||||
type: "file",
|
||||
path: item.path,
|
||||
selection: item.selection,
|
||||
comment: item.comment,
|
||||
commentID: item.commentID,
|
||||
preview: item.preview,
|
||||
})
|
||||
}
|
||||
restoreInput()
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1391,7 +1592,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
/>
|
||||
<div class="flex items-center text-14-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap truncate min-w-0">
|
||||
{getDirectory((item as { type: "file"; path: string }).path)}
|
||||
{(() => {
|
||||
const path = (item as { type: "file"; path: string }).path
|
||||
return path.endsWith("/") ? path : getDirectory(path)
|
||||
})()}
|
||||
</span>
|
||||
<Show when={!(item as { type: "file"; path: string }).path.endsWith("/")}>
|
||||
<span class="text-text-strong whitespace-nowrap">
|
||||
@@ -1470,63 +1674,57 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={false && (prompt.context.items().length > 0 || !!activeFile())}>
|
||||
<div class="flex flex-wrap items-center gap-2 px-3 pt-3">
|
||||
<Show when={prompt.context.activeTab() ? activeFile() : undefined}>
|
||||
{(path) => (
|
||||
<div class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base max-w-full">
|
||||
<FileIcon node={{ path: path(), type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-12-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
|
||||
<span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
|
||||
</div>
|
||||
<IconButton
|
||||
type="button"
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
class="h-6 w-6"
|
||||
onClick={() => prompt.context.removeActive()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={!prompt.context.activeTab() && !!activeFile()}>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base text-12-regular text-text-weak hover:bg-surface-raised-base-hover"
|
||||
onClick={() => prompt.context.addActive()}
|
||||
>
|
||||
<Icon name="plus-small" size="small" />
|
||||
<span>{language.t("prompt.context.includeActiveFile")}</span>
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={prompt.context.items().length > 0}>
|
||||
<div class="flex flex-nowrap items-start gap-1.5 px-3 pt-3 overflow-x-auto no-scrollbar">
|
||||
<For each={prompt.context.items()}>
|
||||
{(item) => (
|
||||
<div class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base max-w-full">
|
||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-12-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(item.path)}</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path)}</span>
|
||||
<Show when={item.selection}>
|
||||
{(sel) => (
|
||||
<span class="text-text-weak whitespace-nowrap ml-1">
|
||||
{sel().startLine === sel().endLine
|
||||
? `:${sel().startLine}`
|
||||
: `:${sel().startLine}-${sel().endLine}`}
|
||||
</span>
|
||||
)}
|
||||
{(item) => {
|
||||
return (
|
||||
<div
|
||||
classList={{
|
||||
"shrink-0 flex flex-col gap-1 rounded-md bg-surface-base border border-border-base px-2 py-1 max-w-[320px]": true,
|
||||
"cursor-pointer hover:bg-surface-raised-base-hover": !!item.commentID,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!item.commentID) return
|
||||
comments.setFocus({ file: item.path, id: item.commentID })
|
||||
view().reviewPanel.open()
|
||||
tabs().open("review")
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
|
||||
<div class="flex items-center text-11-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(item.path)}</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path)}</span>
|
||||
<Show when={item.selection}>
|
||||
{(sel) => (
|
||||
<span class="text-text-weak whitespace-nowrap ml-1">
|
||||
{sel().startLine === sel().endLine
|
||||
? `:${sel().startLine}`
|
||||
: `:${sel().startLine}-${sel().endLine}`}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<IconButton
|
||||
type="button"
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
class="h-5 w-5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (item.commentID) comments.remove(item.path, item.commentID)
|
||||
prompt.context.remove(item.key)
|
||||
}}
|
||||
aria-label={language.t("prompt.context.removeFile")}
|
||||
/>
|
||||
</div>
|
||||
<Show when={item.comment}>
|
||||
{(comment) => <div class="text-11-regular text-text-strong">{comment()}</div>}
|
||||
</Show>
|
||||
</div>
|
||||
<IconButton
|
||||
type="button"
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
class="h-6 w-6"
|
||||
onClick={() => prompt.context.remove(item.key)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -1556,6 +1754,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
type="button"
|
||||
onClick={() => removeImageAttachment(attachment.id)}
|
||||
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
|
||||
aria-label={language.t("prompt.attachment.remove")}
|
||||
>
|
||||
<Icon name="close" class="size-3 text-text-weak" />
|
||||
</button>
|
||||
@@ -1574,6 +1773,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
editorRef = el
|
||||
props.ref?.(el)
|
||||
}}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
aria-label={
|
||||
store.mode === "shell"
|
||||
? language.t("prompt.placeholder.shell")
|
||||
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })
|
||||
}
|
||||
contenteditable="true"
|
||||
onInput={handleInput}
|
||||
onPaste={handlePaste}
|
||||
@@ -1638,21 +1844,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</TooltipKeybind>
|
||||
}
|
||||
>
|
||||
<ModelSelectorPopover>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button as="div" variant="ghost">
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||
</Show>
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</ModelSelectorPopover>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<ModelSelectorPopover triggerAs={Button} triggerProps={{ variant: "ghost" }}>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||
</Show>
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</ModelSelectorPopover>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<Show when={local.model.variant.list().length > 0}>
|
||||
<TooltipKeybind
|
||||
@@ -1683,6 +1887,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
|
||||
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
||||
}}
|
||||
aria-label={
|
||||
permission.isAutoAccepting(params.id!, sdk.directory)
|
||||
? language.t("command.permissions.autoaccept.disable")
|
||||
: language.t("command.permissions.autoaccept.enable")
|
||||
}
|
||||
aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-double-right"
|
||||
@@ -1711,7 +1921,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<SessionContextUsage />
|
||||
<Show when={store.mode === "normal"}>
|
||||
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
|
||||
<Button type="button" variant="ghost" class="size-6" onClick={() => fileInputRef.click()}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
class="size-6"
|
||||
onClick={() => fileInputRef.click()}
|
||||
aria-label={language.t("prompt.action.attachFile")}
|
||||
>
|
||||
<Icon name="photo" class="size-4.5" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -1743,6 +1959,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
icon={working() ? "stop" : "arrow-up"}
|
||||
variant="primary"
|
||||
class="h-6 w-4.5"
|
||||
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -21,8 +21,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
|
||||
const variant = createMemo(() => props.variant ?? "button")
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
|
||||
const cost = createMemo(() => {
|
||||
@@ -96,7 +96,13 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
<Switch>
|
||||
<Match when={variant() === "indicator"}>{circle()}</Match>
|
||||
<Match when={true}>
|
||||
<Button type="button" variant="ghost" class="size-6" onClick={openContext}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
class="size-6"
|
||||
onClick={openContext}
|
||||
aria-label={language.t("context.usage.view")}
|
||||
>
|
||||
{circle()}
|
||||
</Button>
|
||||
</Match>
|
||||
|
||||
@@ -282,7 +282,9 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
}
|
||||
})
|
||||
|
||||
return <Code file={file()} overflow="wrap" class="select-text" />
|
||||
return (
|
||||
<Code file={file()} overflow="wrap" class="select-text" onRendered={() => requestAnimationFrame(restoreScroll)} />
|
||||
)
|
||||
}
|
||||
|
||||
function RawMessage(msgProps: { message: Message }) {
|
||||
@@ -314,19 +316,13 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
let frame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
|
||||
const restoreScroll = (retries = 0) => {
|
||||
const restoreScroll = () => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
|
||||
const s = props.view()?.scroll("context")
|
||||
if (!s) return
|
||||
|
||||
// Wait for content to be scrollable - content may not have rendered yet
|
||||
if (el.scrollHeight <= el.clientHeight && retries < 10) {
|
||||
requestAnimationFrame(() => restoreScroll(retries + 1))
|
||||
return
|
||||
}
|
||||
|
||||
if (el.scrollTop !== s.y) el.scrollTop = s.y
|
||||
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ export function SessionHeader() {
|
||||
|
||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const showReview = createMemo(() => !!currentSession()?.summary?.files)
|
||||
const showShare = createMemo(() => shareEnabled() && !!currentSession())
|
||||
const showReview = createMemo(() => !!currentSession())
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
|
||||
const [state, setState] = createStore({
|
||||
share: false,
|
||||
@@ -136,6 +136,7 @@ export function SessionHeader() {
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
aria-label={language.t("session.header.searchFiles")}
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible">
|
||||
<Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" />
|
||||
@@ -176,13 +177,7 @@ export function SessionHeader() {
|
||||
{/* <SessionMcpIndicator /> */}
|
||||
{/* </div> */}
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="hidden md:block shrink-0"
|
||||
classList={{
|
||||
"opacity-0 pointer-events-none": !showReview(),
|
||||
}}
|
||||
aria-hidden={!showReview()}
|
||||
>
|
||||
<div class="hidden md:block shrink-0">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.review.toggle")}
|
||||
keybind={command.keybind("review.toggle")}
|
||||
@@ -191,6 +186,10 @@ export function SessionHeader() {
|
||||
variant="ghost"
|
||||
class="group/review-toggle size-6 p-0"
|
||||
onClick={() => view().reviewPanel.toggle()}
|
||||
aria-label={language.t("command.review.toggle")}
|
||||
aria-expanded={view().reviewPanel.opened()}
|
||||
aria-controls="review-panel"
|
||||
tabIndex={showReview() ? 0 : -1}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
@@ -219,8 +218,11 @@ export function SessionHeader() {
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/terminal-toggle size-8 rounded-md"
|
||||
class="group/terminal-toggle size-6 p-0"
|
||||
onClick={() => view().terminal.toggle()}
|
||||
aria-label={language.t("command.terminal.toggle")}
|
||||
aria-expanded={view().terminal.opened()}
|
||||
aria-controls="terminal-panel"
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
@@ -242,97 +244,96 @@ export function SessionHeader() {
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center"
|
||||
classList={{
|
||||
"opacity-0 pointer-events-none": !showShare(),
|
||||
}}
|
||||
aria-hidden={!showShare()}
|
||||
>
|
||||
<Popover
|
||||
title={language.t("session.share.popover.title")}
|
||||
description={
|
||||
shareUrl()
|
||||
? language.t("session.share.popover.description.shared")
|
||||
: language.t("session.share.popover.description.unshared")
|
||||
}
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value={language.t("command.session.share")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
classList={{ "rounded-r-none": shareUrl() !== undefined }}
|
||||
style={{ scale: 1 }}
|
||||
>
|
||||
{language.t("session.share.action.share")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Show
|
||||
when={shareUrl()}
|
||||
fallback={
|
||||
<div class="flex">
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-1/2"
|
||||
onClick={shareSession}
|
||||
disabled={state.share}
|
||||
>
|
||||
{state.share
|
||||
? language.t("session.share.action.publishing")
|
||||
: language.t("session.share.action.publish")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col gap-2 w-72">
|
||||
<TextField value={shareUrl() ?? ""} readOnly copyable class="w-full" />
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
size="large"
|
||||
variant="secondary"
|
||||
class="w-full shadow-none border border-border-weak-base"
|
||||
onClick={unshareSession}
|
||||
disabled={state.unshare}
|
||||
>
|
||||
{state.unshare
|
||||
? language.t("session.share.action.unpublishing")
|
||||
: language.t("session.share.action.unpublish")}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-full"
|
||||
onClick={viewShare}
|
||||
disabled={state.unshare}
|
||||
>
|
||||
{language.t("session.share.action.view")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Popover>
|
||||
<Show when={shareUrl()} fallback={<div class="size-6" aria-hidden="true" />}>
|
||||
<Tooltip
|
||||
value={
|
||||
state.copied ? language.t("session.share.copy.copied") : language.t("session.share.copy.copyLink")
|
||||
<Show when={showShare()}>
|
||||
<div class="flex items-center">
|
||||
<Popover
|
||||
title={language.t("session.share.popover.title")}
|
||||
description={
|
||||
shareUrl()
|
||||
? language.t("session.share.popover.description.shared")
|
||||
: language.t("session.share.popover.description.unshared")
|
||||
}
|
||||
placement="top"
|
||||
gutter={8}
|
||||
triggerAs={Button}
|
||||
triggerProps={{
|
||||
variant: "secondary",
|
||||
classList: { "rounded-r-none": shareUrl() !== undefined },
|
||||
style: { scale: 1 },
|
||||
}}
|
||||
trigger={language.t("session.share.action.share")}
|
||||
>
|
||||
<IconButton
|
||||
icon={state.copied ? "check" : "copy"}
|
||||
variant="secondary"
|
||||
class="rounded-l-none"
|
||||
onClick={copyLink}
|
||||
disabled={state.unshare}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Show
|
||||
when={shareUrl()}
|
||||
fallback={
|
||||
<div class="flex">
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-1/2"
|
||||
onClick={shareSession}
|
||||
disabled={state.share}
|
||||
>
|
||||
{state.share
|
||||
? language.t("session.share.action.publishing")
|
||||
: language.t("session.share.action.publish")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col gap-2 w-72">
|
||||
<TextField value={shareUrl() ?? ""} readOnly copyable class="w-full" />
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
size="large"
|
||||
variant="secondary"
|
||||
class="w-full shadow-none border border-border-weak-base"
|
||||
onClick={unshareSession}
|
||||
disabled={state.unshare}
|
||||
>
|
||||
{state.unshare
|
||||
? language.t("session.share.action.unpublishing")
|
||||
: language.t("session.share.action.unpublish")}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-full"
|
||||
onClick={viewShare}
|
||||
disabled={state.unshare}
|
||||
>
|
||||
{language.t("session.share.action.view")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Popover>
|
||||
<Show when={shareUrl()} fallback={<div class="size-6" aria-hidden="true" />}>
|
||||
<Tooltip
|
||||
value={
|
||||
state.copied
|
||||
? language.t("session.share.copy.copied")
|
||||
: language.t("session.share.copy.copyLink")
|
||||
}
|
||||
placement="top"
|
||||
gutter={8}
|
||||
>
|
||||
<IconButton
|
||||
icon={state.copied ? "check" : "copy"}
|
||||
variant="secondary"
|
||||
class="rounded-l-none"
|
||||
onClick={copyLink}
|
||||
disabled={state.unshare}
|
||||
aria-label={
|
||||
state.copied
|
||||
? language.t("session.share.copy.copied")
|
||||
: language.t("session.share.copy.copyLink")
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
@@ -37,7 +37,12 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
||||
value={props.tab}
|
||||
closeButton={
|
||||
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
|
||||
<IconButton
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
onClick={() => props.onTabClose(props.tab)}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
hideCloseButton
|
||||
|
||||
@@ -139,6 +139,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
||||
e.stopPropagation()
|
||||
close()
|
||||
}}
|
||||
aria-label={language.t("terminal.close")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
||||
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
|
||||
import { Link } from "./link"
|
||||
|
||||
export const SettingsGeneral: Component = () => {
|
||||
const theme = useTheme()
|
||||
@@ -107,9 +108,7 @@ export const SettingsGeneral: Component = () => {
|
||||
description={
|
||||
<>
|
||||
{language.t("settings.general.row.theme.description")}{" "}
|
||||
<a href="#" class="text-text-interactive-base">
|
||||
{language.t("common.learnMore")}
|
||||
</a>
|
||||
<Link href="https://opencode.ai/docs/themes/">{language.t("common.learnMore")}</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -110,7 +110,7 @@ export function Titlebar() {
|
||||
</div>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||
placement="bottom"
|
||||
title={language.t("command.sidebar.toggle")}
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
|
||||
140
packages/app/src/context/comments.tsx
Normal file
140
packages/app/src/context/comments.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import type { SelectedLineRange } from "@/context/file"
|
||||
|
||||
export type LineComment = {
|
||||
id: string
|
||||
file: string
|
||||
selection: SelectedLineRange
|
||||
comment: string
|
||||
time: number
|
||||
}
|
||||
|
||||
type CommentFocus = { file: string; id: string }
|
||||
|
||||
const WORKSPACE_KEY = "__workspace__"
|
||||
const MAX_COMMENT_SESSIONS = 20
|
||||
|
||||
type CommentSession = ReturnType<typeof createCommentSession>
|
||||
|
||||
type CommentCacheEntry = {
|
||||
value: CommentSession
|
||||
dispose: VoidFunction
|
||||
}
|
||||
|
||||
function createCommentSession(dir: string, id: string | undefined) {
|
||||
const legacy = `${dir}/comments${id ? "/" + id : ""}.v1`
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.scoped(dir, id, "comments", [legacy]),
|
||||
createStore<{
|
||||
comments: Record<string, LineComment[]>
|
||||
}>({
|
||||
comments: {},
|
||||
}),
|
||||
)
|
||||
|
||||
const [focus, setFocus] = createSignal<CommentFocus | null>(null)
|
||||
|
||||
const list = (file: string) => store.comments[file] ?? []
|
||||
|
||||
const add = (input: Omit<LineComment, "id" | "time">) => {
|
||||
const next: LineComment = {
|
||||
id: crypto.randomUUID(),
|
||||
time: Date.now(),
|
||||
...input,
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
setStore("comments", input.file, (items) => [...(items ?? []), next])
|
||||
setFocus({ file: input.file, id: next.id })
|
||||
})
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
const remove = (file: string, id: string) => {
|
||||
setStore("comments", file, (items) => (items ?? []).filter((x) => x.id !== id))
|
||||
setFocus((current) => (current?.id === id ? null : current))
|
||||
}
|
||||
|
||||
const all = createMemo(() => {
|
||||
const files = Object.keys(store.comments)
|
||||
const items = files.flatMap((file) => store.comments[file] ?? [])
|
||||
return items.slice().sort((a, b) => a.time - b.time)
|
||||
})
|
||||
|
||||
return {
|
||||
ready,
|
||||
list,
|
||||
all,
|
||||
add,
|
||||
remove,
|
||||
focus: createMemo(() => focus()),
|
||||
setFocus,
|
||||
clearFocus: () => setFocus(null),
|
||||
}
|
||||
}
|
||||
|
||||
export const { use: useComments, provider: CommentsProvider } = createSimpleContext({
|
||||
name: "Comments",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const params = useParams()
|
||||
const cache = new Map<string, CommentCacheEntry>()
|
||||
|
||||
const disposeAll = () => {
|
||||
for (const entry of cache.values()) {
|
||||
entry.dispose()
|
||||
}
|
||||
cache.clear()
|
||||
}
|
||||
|
||||
onCleanup(disposeAll)
|
||||
|
||||
const prune = () => {
|
||||
while (cache.size > MAX_COMMENT_SESSIONS) {
|
||||
const first = cache.keys().next().value
|
||||
if (!first) return
|
||||
const entry = cache.get(first)
|
||||
entry?.dispose()
|
||||
cache.delete(first)
|
||||
}
|
||||
}
|
||||
|
||||
const load = (dir: string, id: string | undefined) => {
|
||||
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
||||
const existing = cache.get(key)
|
||||
if (existing) {
|
||||
cache.delete(key)
|
||||
cache.set(key, existing)
|
||||
return existing.value
|
||||
}
|
||||
|
||||
const entry = createRoot((dispose) => ({
|
||||
value: createCommentSession(dir, id),
|
||||
dispose,
|
||||
}))
|
||||
|
||||
cache.set(key, entry)
|
||||
prune()
|
||||
return entry.value
|
||||
}
|
||||
|
||||
const session = createMemo(() => load(params.dir!, params.id))
|
||||
|
||||
return {
|
||||
ready: () => session().ready(),
|
||||
list: (file: string) => session().list(file),
|
||||
all: () => session().all(),
|
||||
add: (input: Omit<LineComment, "id" | "time">) => session().add(input),
|
||||
remove: (file: string, id: string) => session().remove(file, id),
|
||||
focus: () => session().focus(),
|
||||
setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
|
||||
clearFocus: () => session().clearFocus(),
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -189,26 +189,15 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
const params = useParams()
|
||||
const language = useLanguage()
|
||||
|
||||
const scope = createMemo(() => sdk.directory)
|
||||
|
||||
const directory = createMemo(() => sync.data.path.directory)
|
||||
|
||||
function normalize(input: string) {
|
||||
const root = directory()
|
||||
const prefix = root.endsWith("/") ? root : root + "/"
|
||||
|
||||
let path = input
|
||||
|
||||
// Only strip protocol and decode if it's a file URI
|
||||
if (path.startsWith("file://")) {
|
||||
const raw = stripQueryAndHash(stripFileProtocol(path))
|
||||
try {
|
||||
// Attempt to treat as a standard URI
|
||||
path = decodeURIComponent(raw)
|
||||
} catch {
|
||||
// Fallback for legacy paths that might contain invalid URI sequences (e.g. "100%")
|
||||
// In this case, we treat the path as raw, but still strip the protocol
|
||||
path = raw
|
||||
}
|
||||
}
|
||||
let path = stripQueryAndHash(stripFileProtocol(input))
|
||||
|
||||
if (path.startsWith(prefix)) {
|
||||
path = path.slice(prefix.length)
|
||||
@@ -231,8 +220,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
|
||||
function tab(input: string) {
|
||||
const path = normalize(input)
|
||||
const encoded = path.split("/").map(encodeURIComponent).join("/")
|
||||
return `file://${encoded}`
|
||||
return `file://${path}`
|
||||
}
|
||||
|
||||
function pathFromTab(tabValue: string) {
|
||||
@@ -248,6 +236,12 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
file: {},
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
scope()
|
||||
inflight.clear()
|
||||
setStore("file", {})
|
||||
})
|
||||
|
||||
const viewCache = new Map<string, ViewCacheEntry>()
|
||||
|
||||
const disposeViews = () => {
|
||||
@@ -298,12 +292,16 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
const path = normalize(input)
|
||||
if (!path) return Promise.resolve()
|
||||
|
||||
const directory = scope()
|
||||
const key = `${directory}\n${path}`
|
||||
const client = sdk.client
|
||||
|
||||
ensure(path)
|
||||
|
||||
const current = store.file[path]
|
||||
if (!options?.force && current?.loaded) return Promise.resolve()
|
||||
|
||||
const pending = inflight.get(path)
|
||||
const pending = inflight.get(key)
|
||||
if (pending) return pending
|
||||
|
||||
setStore(
|
||||
@@ -315,9 +313,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
}),
|
||||
)
|
||||
|
||||
const promise = sdk.client.file
|
||||
const promise = client.file
|
||||
.read({ path })
|
||||
.then((x) => {
|
||||
if (scope() !== directory) return
|
||||
setStore(
|
||||
"file",
|
||||
path,
|
||||
@@ -329,6 +328,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
if (scope() !== directory) return
|
||||
setStore(
|
||||
"file",
|
||||
path,
|
||||
@@ -344,10 +344,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
inflight.delete(path)
|
||||
inflight.delete(key)
|
||||
})
|
||||
|
||||
inflight.set(path, promise)
|
||||
inflight.set(key, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
batch,
|
||||
createContext,
|
||||
createEffect,
|
||||
untrack,
|
||||
getOwner,
|
||||
runWithOwner,
|
||||
useContext,
|
||||
@@ -44,11 +45,24 @@ import { usePlatform } from "./platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
type ProjectMeta = {
|
||||
name?: string
|
||||
icon?: {
|
||||
override?: string
|
||||
color?: string
|
||||
}
|
||||
commands?: {
|
||||
start?: string
|
||||
}
|
||||
}
|
||||
|
||||
type State = {
|
||||
status: "loading" | "partial" | "complete"
|
||||
agent: Agent[]
|
||||
command: Command[]
|
||||
project: string
|
||||
projectMeta: ProjectMeta | undefined
|
||||
icon: string | undefined
|
||||
provider: ProviderListResponse
|
||||
config: Config
|
||||
path: Path
|
||||
@@ -89,6 +103,18 @@ type VcsCache = {
|
||||
ready: Accessor<boolean>
|
||||
}
|
||||
|
||||
type MetaCache = {
|
||||
store: Store<{ value: ProjectMeta | undefined }>
|
||||
setStore: SetStoreFunction<{ value: ProjectMeta | undefined }>
|
||||
ready: Accessor<boolean>
|
||||
}
|
||||
|
||||
type IconCache = {
|
||||
store: Store<{ value: string | undefined }>
|
||||
setStore: SetStoreFunction<{ value: string | undefined }>
|
||||
ready: Accessor<boolean>
|
||||
}
|
||||
|
||||
type ChildOptions = {
|
||||
bootstrap?: boolean
|
||||
}
|
||||
@@ -100,6 +126,25 @@ function createGlobalSync() {
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||
const vcsCache = new Map<string, VcsCache>()
|
||||
const metaCache = new Map<string, MetaCache>()
|
||||
const iconCache = new Map<string, IconCache>()
|
||||
|
||||
const [projectCache, setProjectCache, , projectCacheReady] = persisted(
|
||||
Persist.global("globalSync.project", ["globalSync.project.v1"]),
|
||||
createStore({ value: [] as Project[] }),
|
||||
)
|
||||
|
||||
const sanitizeProject = (project: Project) => {
|
||||
if (!project.icon?.url && !project.icon?.override) return project
|
||||
return {
|
||||
...project,
|
||||
icon: {
|
||||
...project.icon,
|
||||
url: undefined,
|
||||
override: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
const [globalStore, setGlobalStore] = createStore<{
|
||||
ready: boolean
|
||||
error?: InitError
|
||||
@@ -112,7 +157,7 @@ function createGlobalSync() {
|
||||
}>({
|
||||
ready: false,
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
project: [],
|
||||
project: projectCache.value,
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
provider_auth: {},
|
||||
config: {},
|
||||
@@ -120,6 +165,24 @@ function createGlobalSync() {
|
||||
})
|
||||
let bootstrapQueue: string[] = []
|
||||
|
||||
createEffect(() => {
|
||||
if (!projectCacheReady()) return
|
||||
if (globalStore.project.length !== 0) return
|
||||
const cached = projectCache.value
|
||||
if (cached.length === 0) return
|
||||
setGlobalStore("project", cached)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!projectCacheReady()) return
|
||||
const projects = globalStore.project
|
||||
if (projects.length === 0) {
|
||||
const cachedLength = untrack(() => projectCache.value.length)
|
||||
if (cachedLength !== 0) return
|
||||
}
|
||||
setProjectCache("value", projects.map(sanitizeProject))
|
||||
})
|
||||
|
||||
createEffect(async () => {
|
||||
if (globalStore.reload !== "complete") return
|
||||
if (bootstrapQueue.length) {
|
||||
@@ -149,9 +212,29 @@ function createGlobalSync() {
|
||||
if (!cache) throw new Error("Failed to create persisted cache")
|
||||
vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
|
||||
|
||||
const meta = runWithOwner(owner, () =>
|
||||
persisted(
|
||||
Persist.workspace(directory, "project", ["project.v1"]),
|
||||
createStore({ value: undefined as ProjectMeta | undefined }),
|
||||
),
|
||||
)
|
||||
if (!meta) throw new Error("Failed to create persisted project metadata")
|
||||
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
||||
|
||||
const icon = runWithOwner(owner, () =>
|
||||
persisted(
|
||||
Persist.workspace(directory, "icon", ["icon.v1"]),
|
||||
createStore({ value: undefined as string | undefined }),
|
||||
),
|
||||
)
|
||||
if (!icon) throw new Error("Failed to create persisted project icon")
|
||||
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
||||
|
||||
const init = () => {
|
||||
children[directory] = createStore<State>({
|
||||
const child = createStore<State>({
|
||||
project: "",
|
||||
projectMeta: meta[0].value,
|
||||
icon: icon[0].value,
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
@@ -172,6 +255,16 @@ function createGlobalSync() {
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
|
||||
children[directory] = child
|
||||
|
||||
createEffect(() => {
|
||||
child[1]("projectMeta", meta[0].value)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
child[1]("icon", icon[0].value)
|
||||
})
|
||||
}
|
||||
|
||||
runWithOwner(owner, init)
|
||||
@@ -253,6 +346,8 @@ function createGlobalSync() {
|
||||
const [store, setStore] = ensureChild(directory)
|
||||
const cache = vcsCache.get(directory)
|
||||
if (!cache) return
|
||||
const meta = metaCache.get(directory)
|
||||
if (!meta) return
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
fetch: platform.fetch,
|
||||
@@ -269,6 +364,8 @@ function createGlobalSync() {
|
||||
setStore("vcs", (value) => value ?? cached)
|
||||
})
|
||||
|
||||
// projectMeta is synced from persisted storage in ensureChild.
|
||||
|
||||
const blockingRequests = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () =>
|
||||
@@ -475,6 +572,20 @@ function createGlobalSync() {
|
||||
)
|
||||
break
|
||||
}
|
||||
case "session.deleted": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (event.properties.info.parentID) break
|
||||
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
break
|
||||
}
|
||||
case "session.diff":
|
||||
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
|
||||
break
|
||||
@@ -711,6 +822,32 @@ function createGlobalSync() {
|
||||
bootstrap()
|
||||
})
|
||||
|
||||
function projectMeta(directory: string, patch: ProjectMeta) {
|
||||
const [store, setStore] = ensureChild(directory)
|
||||
const cached = metaCache.get(directory)
|
||||
if (!cached) return
|
||||
const previous = store.projectMeta ?? {}
|
||||
const icon = patch.icon ? { ...(previous.icon ?? {}), ...patch.icon } : previous.icon
|
||||
const commands = patch.commands ? { ...(previous.commands ?? {}), ...patch.commands } : previous.commands
|
||||
const next = {
|
||||
...previous,
|
||||
...patch,
|
||||
icon,
|
||||
commands,
|
||||
}
|
||||
cached.setStore("value", next)
|
||||
setStore("projectMeta", next)
|
||||
}
|
||||
|
||||
function projectIcon(directory: string, value: string | undefined) {
|
||||
const [store, setStore] = ensureChild(directory)
|
||||
const cached = iconCache.get(directory)
|
||||
if (!cached) return
|
||||
if (store.icon === value) return
|
||||
cached.setStore("value", value)
|
||||
setStore("icon", value)
|
||||
}
|
||||
|
||||
return {
|
||||
data: globalStore,
|
||||
set: setGlobalStore,
|
||||
@@ -732,6 +869,8 @@ function createGlobalSync() {
|
||||
},
|
||||
project: {
|
||||
loadSessions,
|
||||
meta: projectMeta,
|
||||
icon: projectIcon,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import { dict as da } from "@/i18n/da"
|
||||
import { dict as ja } from "@/i18n/ja"
|
||||
import { dict as pl } from "@/i18n/pl"
|
||||
import { dict as ru } from "@/i18n/ru"
|
||||
import { dict as ar } from "@/i18n/ar"
|
||||
import { dict as no } from "@/i18n/no"
|
||||
import { dict as br } from "@/i18n/br"
|
||||
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
|
||||
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
|
||||
@@ -25,13 +28,16 @@ import { dict as uiDa } from "@opencode-ai/ui/i18n/da"
|
||||
import { dict as uiJa } from "@opencode-ai/ui/i18n/ja"
|
||||
import { dict as uiPl } from "@opencode-ai/ui/i18n/pl"
|
||||
import { dict as uiRu } from "@opencode-ai/ui/i18n/ru"
|
||||
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
|
||||
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
|
||||
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
|
||||
|
||||
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru"
|
||||
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
|
||||
|
||||
type RawDictionary = typeof en & typeof uiEn
|
||||
type Dictionary = i18n.Flatten<RawDictionary>
|
||||
|
||||
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru"]
|
||||
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
|
||||
|
||||
function detectLocale(): Locale {
|
||||
if (typeof navigator !== "object") return "en"
|
||||
@@ -51,6 +57,14 @@ function detectLocale(): Locale {
|
||||
if (language.toLowerCase().startsWith("ja")) return "ja"
|
||||
if (language.toLowerCase().startsWith("pl")) return "pl"
|
||||
if (language.toLowerCase().startsWith("ru")) return "ru"
|
||||
if (language.toLowerCase().startsWith("ar")) return "ar"
|
||||
if (
|
||||
language.toLowerCase().startsWith("no") ||
|
||||
language.toLowerCase().startsWith("nb") ||
|
||||
language.toLowerCase().startsWith("nn")
|
||||
)
|
||||
return "no"
|
||||
if (language.toLowerCase().startsWith("pt")) return "br"
|
||||
}
|
||||
|
||||
return "en"
|
||||
@@ -77,6 +91,9 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
if (store.locale === "ja") return "ja"
|
||||
if (store.locale === "pl") return "pl"
|
||||
if (store.locale === "ru") return "ru"
|
||||
if (store.locale === "ar") return "ar"
|
||||
if (store.locale === "no") return "no"
|
||||
if (store.locale === "br") return "br"
|
||||
return "en"
|
||||
})
|
||||
|
||||
@@ -98,6 +115,9 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
if (locale() === "ja") return { ...base, ...i18n.flatten({ ...ja, ...uiJa }) }
|
||||
if (locale() === "pl") return { ...base, ...i18n.flatten({ ...pl, ...uiPl }) }
|
||||
if (locale() === "ru") return { ...base, ...i18n.flatten({ ...ru, ...uiRu }) }
|
||||
if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }
|
||||
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
|
||||
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
|
||||
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
|
||||
})
|
||||
|
||||
@@ -115,6 +135,9 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
ja: "language.ja",
|
||||
pl: "language.pl",
|
||||
ru: "language.ru",
|
||||
ar: "language.ar",
|
||||
no: "language.no",
|
||||
br: "language.br",
|
||||
}
|
||||
|
||||
const label = (value: Locale) => t(labelKey[value])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { batch, createEffect, createMemo, on, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
@@ -222,15 +222,38 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const metadata = projectID
|
||||
? globalSync.data.project.find((x) => x.id === projectID)
|
||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
return {
|
||||
|
||||
const local = childStore.projectMeta
|
||||
const localOverride =
|
||||
local?.name !== undefined ||
|
||||
local?.commands?.start !== undefined ||
|
||||
local?.icon?.override !== undefined ||
|
||||
local?.icon?.color !== undefined
|
||||
|
||||
const base = {
|
||||
...(metadata ?? {}),
|
||||
...project,
|
||||
icon: {
|
||||
url: metadata?.icon?.url,
|
||||
override: metadata?.icon?.override,
|
||||
override: metadata?.icon?.override ?? childStore.icon,
|
||||
color: metadata?.icon?.color,
|
||||
},
|
||||
}
|
||||
|
||||
const isGlobal = projectID === "global" || (metadata?.id === undefined && localOverride)
|
||||
if (!isGlobal) return base
|
||||
|
||||
return {
|
||||
...base,
|
||||
id: base.id ?? "global",
|
||||
name: local?.name,
|
||||
commands: local?.commands,
|
||||
icon: {
|
||||
url: base.icon?.url,
|
||||
override: local?.icon?.override,
|
||||
color: local?.icon?.color,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const roots = createMemo(() => {
|
||||
@@ -283,6 +306,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const projects = enriched()
|
||||
if (projects.length === 0) return
|
||||
|
||||
if (globalSync.ready) {
|
||||
for (const project of projects) {
|
||||
if (!project.id) continue
|
||||
if (project.id === "global") continue
|
||||
globalSync.project.icon(project.worktree, project.icon?.override)
|
||||
}
|
||||
}
|
||||
|
||||
const used = new Set<string>()
|
||||
for (const project of projects) {
|
||||
const color = project.icon?.color ?? colors[project.worktree]
|
||||
@@ -291,11 +322,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
|
||||
for (const project of projects) {
|
||||
if (project.icon?.color) continue
|
||||
if (colors[project.worktree]) continue
|
||||
const color = pickAvailableColor(used)
|
||||
used.add(color)
|
||||
setColors(project.worktree, color)
|
||||
const existing = colors[project.worktree]
|
||||
const color = existing ?? pickAvailableColor(used)
|
||||
if (!existing) {
|
||||
used.add(color)
|
||||
setColors(project.worktree, color)
|
||||
}
|
||||
if (!project.id) continue
|
||||
if (project.id === "global") {
|
||||
globalSync.project.meta(project.worktree, { icon: { color } })
|
||||
continue
|
||||
}
|
||||
void globalSdk.client.project.update({ projectID: project.id, directory: project.worktree, icon: { color } })
|
||||
}
|
||||
})
|
||||
@@ -395,10 +432,24 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
setStore("mobileSidebar", "opened", (x) => !x)
|
||||
},
|
||||
},
|
||||
view(sessionKey: string) {
|
||||
touch(sessionKey)
|
||||
scroll.seed(sessionKey)
|
||||
const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} })
|
||||
view(sessionKey: string | Accessor<string>) {
|
||||
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||
|
||||
touch(key())
|
||||
scroll.seed(key())
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
key,
|
||||
(value) => {
|
||||
touch(value)
|
||||
scroll.seed(value)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
||||
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
||||
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
|
||||
|
||||
@@ -428,10 +479,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
|
||||
return {
|
||||
scroll(tab: string) {
|
||||
return scroll.scroll(sessionKey, tab)
|
||||
return scroll.scroll(key(), tab)
|
||||
},
|
||||
setScroll(tab: string, pos: SessionScroll) {
|
||||
scroll.setScroll(sessionKey, tab, pos)
|
||||
scroll.setScroll(key(), tab, pos)
|
||||
},
|
||||
terminal: {
|
||||
opened: terminalOpened,
|
||||
@@ -460,9 +511,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
review: {
|
||||
open: createMemo(() => s().reviewOpen),
|
||||
setOpen(open: string[]) {
|
||||
const current = store.sessionView[sessionKey]
|
||||
const session = key()
|
||||
const current = store.sessionView[session]
|
||||
if (!current) {
|
||||
setStore("sessionView", sessionKey, {
|
||||
setStore("sessionView", session, {
|
||||
scroll: {},
|
||||
reviewOpen: open,
|
||||
})
|
||||
@@ -470,93 +522,111 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
|
||||
if (same(current.reviewOpen, open)) return
|
||||
setStore("sessionView", sessionKey, "reviewOpen", open)
|
||||
setStore("sessionView", session, "reviewOpen", open)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
tabs(sessionKey: string) {
|
||||
touch(sessionKey)
|
||||
const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
|
||||
tabs(sessionKey: string | Accessor<string>) {
|
||||
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||
|
||||
touch(key())
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
key,
|
||||
(value) => {
|
||||
touch(value)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
||||
return {
|
||||
tabs,
|
||||
active: createMemo(() => tabs().active),
|
||||
all: createMemo(() => tabs().all),
|
||||
setActive(tab: string | undefined) {
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all: [], active: tab })
|
||||
const session = key()
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all: [], active: tab })
|
||||
} else {
|
||||
setStore("sessionTabs", sessionKey, "active", tab)
|
||||
setStore("sessionTabs", session, "active", tab)
|
||||
}
|
||||
},
|
||||
setAll(all: string[]) {
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all, active: undefined })
|
||||
const session = key()
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all, active: undefined })
|
||||
} else {
|
||||
setStore("sessionTabs", sessionKey, "all", all)
|
||||
setStore("sessionTabs", session, "all", all)
|
||||
}
|
||||
},
|
||||
async open(tab: string) {
|
||||
const current = store.sessionTabs[sessionKey] ?? { all: [] }
|
||||
const session = key()
|
||||
const current = store.sessionTabs[session] ?? { all: [] }
|
||||
|
||||
if (tab === "review") {
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all: [], active: tab })
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all: [], active: tab })
|
||||
return
|
||||
}
|
||||
setStore("sessionTabs", sessionKey, "active", tab)
|
||||
setStore("sessionTabs", session, "active", tab)
|
||||
return
|
||||
}
|
||||
|
||||
if (tab === "context") {
|
||||
const all = [tab, ...current.all.filter((x) => x !== tab)]
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all, active: tab })
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all, active: tab })
|
||||
return
|
||||
}
|
||||
setStore("sessionTabs", sessionKey, "all", all)
|
||||
setStore("sessionTabs", sessionKey, "active", tab)
|
||||
setStore("sessionTabs", session, "all", all)
|
||||
setStore("sessionTabs", session, "active", tab)
|
||||
return
|
||||
}
|
||||
|
||||
if (!current.all.includes(tab)) {
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all: [tab], active: tab })
|
||||
return
|
||||
}
|
||||
setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
|
||||
setStore("sessionTabs", sessionKey, "active", tab)
|
||||
setStore("sessionTabs", session, "all", [...current.all, tab])
|
||||
setStore("sessionTabs", session, "active", tab)
|
||||
return
|
||||
}
|
||||
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all: current.all, active: tab })
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all: current.all, active: tab })
|
||||
return
|
||||
}
|
||||
setStore("sessionTabs", sessionKey, "active", tab)
|
||||
setStore("sessionTabs", session, "active", tab)
|
||||
},
|
||||
close(tab: string) {
|
||||
const current = store.sessionTabs[sessionKey]
|
||||
const session = key()
|
||||
const current = store.sessionTabs[session]
|
||||
if (!current) return
|
||||
|
||||
const all = current.all.filter((x) => x !== tab)
|
||||
batch(() => {
|
||||
setStore("sessionTabs", sessionKey, "all", all)
|
||||
setStore("sessionTabs", session, "all", all)
|
||||
if (current.active !== tab) return
|
||||
|
||||
const index = current.all.findIndex((f) => f === tab)
|
||||
const next = all[index - 1] ?? all[0]
|
||||
setStore("sessionTabs", sessionKey, "active", next)
|
||||
setStore("sessionTabs", session, "active", next)
|
||||
})
|
||||
},
|
||||
move(tab: string, to: number) {
|
||||
const current = store.sessionTabs[sessionKey]
|
||||
const session = key()
|
||||
const current = store.sessionTabs[session]
|
||||
if (!current) return
|
||||
const index = current.all.findIndex((f) => f === tab)
|
||||
if (index === -1) return
|
||||
setStore(
|
||||
"sessionTabs",
|
||||
sessionKey,
|
||||
session,
|
||||
"all",
|
||||
produce((opened) => {
|
||||
opened.splice(to, 0, opened.splice(index, 1)[0])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { batch, createMemo, onCleanup } from "solid-js"
|
||||
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
@@ -338,6 +338,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
|
||||
})
|
||||
|
||||
const scope = createMemo(() => sdk.directory)
|
||||
createEffect(() => {
|
||||
scope()
|
||||
setStore("node", {})
|
||||
})
|
||||
|
||||
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
|
||||
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
|
||||
|
||||
@@ -394,10 +400,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
|
||||
|
||||
const load = async (path: string) => {
|
||||
const directory = scope()
|
||||
const client = sdk.client
|
||||
const relativePath = relative(path)
|
||||
await sdk.client.file
|
||||
await client.file
|
||||
.read({ path: relativePath })
|
||||
.then((x) => {
|
||||
if (scope() !== directory) return
|
||||
if (!store.node[relativePath]) return
|
||||
setStore(
|
||||
"node",
|
||||
@@ -409,6 +418,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
if (scope() !== directory) return
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.file.loadFailed.title"),
|
||||
@@ -453,9 +463,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
|
||||
const list = async (path: string) => {
|
||||
return sdk.client.file
|
||||
const directory = scope()
|
||||
const client = sdk.client
|
||||
return client.file
|
||||
.list({ path: path + "/" })
|
||||
.then((x) => {
|
||||
if (scope() !== directory) return
|
||||
setStore(
|
||||
"node",
|
||||
produce((draft) => {
|
||||
|
||||
@@ -46,6 +46,9 @@ export type Platform = {
|
||||
|
||||
/** Set the default server URL to use on app startup (desktop only) */
|
||||
setDefaultServerUrl?(url: string | null): Promise<void>
|
||||
|
||||
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
|
||||
parseMarkdown?(markdown: string): Promise<string>
|
||||
}
|
||||
|
||||
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import type { FileSelection } from "@/context/file"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
|
||||
interface PartBase {
|
||||
content: string
|
||||
@@ -41,6 +42,9 @@ export type FileContextItem = {
|
||||
type: "file"
|
||||
path: string
|
||||
selection?: FileSelection
|
||||
comment?: string
|
||||
commentID?: string
|
||||
preview?: string
|
||||
}
|
||||
|
||||
export type ContextItem = FileContextItem
|
||||
@@ -118,14 +122,12 @@ function createPromptSession(dir: string, id: string | undefined) {
|
||||
prompt: Prompt
|
||||
cursor?: number
|
||||
context: {
|
||||
activeTab: boolean
|
||||
items: (ContextItem & { key: string })[]
|
||||
}
|
||||
}>({
|
||||
prompt: clonePrompt(DEFAULT_PROMPT),
|
||||
cursor: undefined,
|
||||
context: {
|
||||
activeTab: true,
|
||||
items: [],
|
||||
},
|
||||
}),
|
||||
@@ -135,7 +137,16 @@ function createPromptSession(dir: string, id: string | undefined) {
|
||||
if (item.type !== "file") return item.type
|
||||
const start = item.selection?.startLine
|
||||
const end = item.selection?.endLine
|
||||
return `${item.type}:${item.path}:${start}:${end}`
|
||||
const key = `${item.type}:${item.path}:${start}:${end}`
|
||||
|
||||
if (item.commentID) {
|
||||
return `${key}:c=${item.commentID}`
|
||||
}
|
||||
|
||||
const comment = item.comment?.trim()
|
||||
if (!comment) return key
|
||||
const digest = checksum(comment) ?? comment
|
||||
return `${key}:c=${digest.slice(0, 8)}`
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -144,14 +155,7 @@ function createPromptSession(dir: string, id: string | undefined) {
|
||||
cursor: createMemo(() => store.cursor),
|
||||
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
||||
context: {
|
||||
activeTab: createMemo(() => store.context.activeTab),
|
||||
items: createMemo(() => store.context.items),
|
||||
addActive() {
|
||||
setStore("context", "activeTab", true)
|
||||
},
|
||||
removeActive() {
|
||||
setStore("context", "activeTab", false)
|
||||
},
|
||||
add(item: ContextItem) {
|
||||
const key = keyForItem(item)
|
||||
if (store.context.items.find((x) => x.key === key)) return
|
||||
@@ -230,10 +234,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
||||
cursor: () => session().cursor(),
|
||||
dirty: () => session().dirty(),
|
||||
context: {
|
||||
activeTab: () => session().context.activeTab(),
|
||||
items: () => session().context.items(),
|
||||
addActive: () => session().context.addActive(),
|
||||
removeActive: () => session().context.removeActive(),
|
||||
add: (item: ContextItem) => session().context.add(item),
|
||||
remove: (key: string) => session().context.remove(key),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { onCleanup } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { usePlatform } from "./platform"
|
||||
|
||||
@@ -10,22 +10,39 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
init: (props: { directory: string }) => {
|
||||
const platform = usePlatform()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
fetch: platform.fetch,
|
||||
directory: props.directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
const directory = createMemo(() => props.directory)
|
||||
const client = createMemo(() =>
|
||||
createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
fetch: platform.fetch,
|
||||
directory: directory(),
|
||||
throwOnError: true,
|
||||
}),
|
||||
)
|
||||
|
||||
const emitter = createGlobalEmitter<{
|
||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||
}>()
|
||||
|
||||
const unsub = globalSDK.event.on(props.directory, (event) => {
|
||||
emitter.emit(event.type, event)
|
||||
createEffect(() => {
|
||||
const unsub = globalSDK.event.on(directory(), (event) => {
|
||||
emitter.emit(event.type, event)
|
||||
})
|
||||
onCleanup(unsub)
|
||||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
|
||||
return {
|
||||
get directory() {
|
||||
return directory()
|
||||
},
|
||||
get client() {
|
||||
return client()
|
||||
},
|
||||
event: emitter,
|
||||
get url() {
|
||||
return globalSDK.url
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -60,16 +60,16 @@ const monoFallback =
|
||||
|
||||
const monoFonts: Record<string, string> = {
|
||||
"ibm-plex-mono": `"IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"cascadia-code": `"Cascadia Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"fira-code": `"Fira Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
hack: `"Hack Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
inconsolata: `"Inconsolata Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"intel-one-mono": `"Intel One Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"jetbrains-mono": `"JetBrains Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"meslo-lgs": `"Meslo LGS Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"roboto-mono": `"Roboto Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"source-code-pro": `"Source Code Pro Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"ubuntu-mono": `"Ubuntu Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"cascadia-code": `"Cascadia Code Nerd Font", "Cascadia Code NF", "Cascadia Mono NF", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"fira-code": `"Fira Code Nerd Font", "FiraMono Nerd Font", "FiraMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
hack: `"Hack Nerd Font", "Hack Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
inconsolata: `"Inconsolata Nerd Font", "Inconsolata Nerd Font Mono","IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"intel-one-mono": `"Intel One Mono Nerd Font", "IntoneMono Nerd Font", "IntoneMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"jetbrains-mono": `"JetBrains Mono Nerd Font", "JetBrainsMono Nerd Font Mono", "JetBrainsMonoNL Nerd Font", "JetBrainsMonoNL Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"meslo-lgs": `"Meslo LGS Nerd Font", "MesloLGS Nerd Font", "MesloLGM Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"roboto-mono": `"Roboto Mono Nerd Font", "RobotoMono Nerd Font", "RobotoMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"source-code-pro": `"Source Code Pro Nerd Font", "SauceCodePro Nerd Font", "SauceCodePro Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"ubuntu-mono": `"Ubuntu Mono Nerd Font", "UbuntuMono Nerd Font", "UbuntuMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
}
|
||||
|
||||
export function monoFontFamily(font: string | undefined) {
|
||||
|
||||
@@ -7,13 +7,20 @@ import { useGlobalSync } from "./global-sync"
|
||||
import { useSDK } from "./sdk"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
init: () => {
|
||||
const globalSync = useGlobalSync()
|
||||
const sdk = useSDK()
|
||||
const [store, setStore] = globalSync.child(sdk.directory)
|
||||
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
|
||||
|
||||
type Child = ReturnType<(typeof globalSync)["child"]>
|
||||
type Store = Child[0]
|
||||
type Setter = Child[1]
|
||||
|
||||
const current = createMemo(() => globalSync.child(sdk.directory))
|
||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||
const chunk = 400
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
@@ -25,6 +32,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const getSession = (sessionID: string) => {
|
||||
const store = current()[0]
|
||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
if (match.found) return store.session[match.index]
|
||||
return undefined
|
||||
@@ -35,22 +43,30 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return Math.ceil(count / chunk) * chunk
|
||||
}
|
||||
|
||||
const hydrateMessages = (sessionID: string) => {
|
||||
if (meta.limit[sessionID] !== undefined) return
|
||||
const hydrateMessages = (directory: string, store: Store, sessionID: string) => {
|
||||
const key = keyFor(directory, sessionID)
|
||||
if (meta.limit[key] !== undefined) return
|
||||
|
||||
const messages = store.message[sessionID]
|
||||
if (!messages) return
|
||||
|
||||
const limit = limitFor(messages.length)
|
||||
setMeta("limit", sessionID, limit)
|
||||
setMeta("complete", sessionID, messages.length < limit)
|
||||
setMeta("limit", key, limit)
|
||||
setMeta("complete", key, messages.length < limit)
|
||||
}
|
||||
|
||||
const loadMessages = async (sessionID: string, limit: number) => {
|
||||
if (meta.loading[sessionID]) return
|
||||
const loadMessages = async (input: {
|
||||
directory: string
|
||||
client: typeof sdk.client
|
||||
setStore: Setter
|
||||
sessionID: string
|
||||
limit: number
|
||||
}) => {
|
||||
const key = keyFor(input.directory, input.sessionID)
|
||||
if (meta.loading[key]) return
|
||||
|
||||
setMeta("loading", sessionID, true)
|
||||
await retry(() => sdk.client.session.messages({ sessionID, limit }))
|
||||
setMeta("loading", key, true)
|
||||
await retry(() => input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }))
|
||||
.then((messages) => {
|
||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
||||
const next = items
|
||||
@@ -60,10 +76,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
batch(() => {
|
||||
setStore("message", sessionID, reconcile(next, { key: "id" }))
|
||||
input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
|
||||
|
||||
for (const message of items) {
|
||||
setStore(
|
||||
input.setStore(
|
||||
"part",
|
||||
message.info.id,
|
||||
reconcile(
|
||||
@@ -76,25 +92,30 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
)
|
||||
}
|
||||
|
||||
setMeta("limit", sessionID, limit)
|
||||
setMeta("complete", sessionID, next.length < limit)
|
||||
setMeta("limit", key, input.limit)
|
||||
setMeta("complete", key, next.length < input.limit)
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
setMeta("loading", sessionID, false)
|
||||
setMeta("loading", key, false)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
data: store,
|
||||
set: setStore,
|
||||
get data() {
|
||||
return current()[0]
|
||||
},
|
||||
get set(): Setter {
|
||||
return current()[1]
|
||||
},
|
||||
get status() {
|
||||
return store.status
|
||||
return current()[0].status
|
||||
},
|
||||
get ready() {
|
||||
return store.status !== "loading"
|
||||
return current()[0].status !== "loading"
|
||||
},
|
||||
get project() {
|
||||
const store = current()[0]
|
||||
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
|
||||
if (match.found) return globalSync.data.project[match.index]
|
||||
return undefined
|
||||
@@ -116,7 +137,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
}
|
||||
setStore(
|
||||
current()[1](
|
||||
produce((draft) => {
|
||||
const messages = draft.message[input.sessionID]
|
||||
if (!messages) {
|
||||
@@ -133,20 +154,28 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
)
|
||||
},
|
||||
async sync(sessionID: string) {
|
||||
const hasSession = getSession(sessionID) !== undefined
|
||||
hydrateMessages(sessionID)
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const hasSession = (() => {
|
||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
return match.found
|
||||
})()
|
||||
|
||||
hydrateMessages(directory, store, sessionID)
|
||||
|
||||
const hasMessages = store.message[sessionID] !== undefined
|
||||
if (hasSession && hasMessages) return
|
||||
|
||||
const pending = inflight.get(sessionID)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const pending = inflight.get(key)
|
||||
if (pending) return pending
|
||||
|
||||
const limit = meta.limit[sessionID] ?? chunk
|
||||
const limit = meta.limit[key] ?? chunk
|
||||
|
||||
const sessionReq = hasSession
|
||||
? Promise.resolve()
|
||||
: retry(() => sdk.client.session.get({ sessionID })).then((session) => {
|
||||
: retry(() => client.session.get({ sessionID })).then((session) => {
|
||||
const data = session.data
|
||||
if (!data) return
|
||||
setStore(
|
||||
@@ -162,72 +191,104 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
)
|
||||
})
|
||||
|
||||
const messagesReq = hasMessages ? Promise.resolve() : loadMessages(sessionID, limit)
|
||||
const messagesReq = hasMessages
|
||||
? Promise.resolve()
|
||||
: loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
|
||||
const promise = Promise.all([sessionReq, messagesReq])
|
||||
.then(() => {})
|
||||
.finally(() => {
|
||||
inflight.delete(sessionID)
|
||||
inflight.delete(key)
|
||||
})
|
||||
|
||||
inflight.set(sessionID, promise)
|
||||
inflight.set(key, promise)
|
||||
return promise
|
||||
},
|
||||
async diff(sessionID: string) {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
if (store.session_diff[sessionID] !== undefined) return
|
||||
|
||||
const pending = inflightDiff.get(sessionID)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const pending = inflightDiff.get(key)
|
||||
if (pending) return pending
|
||||
|
||||
const promise = retry(() => sdk.client.session.diff({ sessionID }))
|
||||
const promise = retry(() => client.session.diff({ sessionID }))
|
||||
.then((diff) => {
|
||||
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
||||
})
|
||||
.finally(() => {
|
||||
inflightDiff.delete(sessionID)
|
||||
inflightDiff.delete(key)
|
||||
})
|
||||
|
||||
inflightDiff.set(sessionID, promise)
|
||||
inflightDiff.set(key, promise)
|
||||
return promise
|
||||
},
|
||||
async todo(sessionID: string) {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
if (store.todo[sessionID] !== undefined) return
|
||||
|
||||
const pending = inflightTodo.get(sessionID)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const pending = inflightTodo.get(key)
|
||||
if (pending) return pending
|
||||
|
||||
const promise = retry(() => sdk.client.session.todo({ sessionID }))
|
||||
const promise = retry(() => client.session.todo({ sessionID }))
|
||||
.then((todo) => {
|
||||
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
|
||||
})
|
||||
.finally(() => {
|
||||
inflightTodo.delete(sessionID)
|
||||
inflightTodo.delete(key)
|
||||
})
|
||||
|
||||
inflightTodo.set(sessionID, promise)
|
||||
inflightTodo.set(key, promise)
|
||||
return promise
|
||||
},
|
||||
history: {
|
||||
more(sessionID: string) {
|
||||
const store = current()[0]
|
||||
const key = keyFor(sdk.directory, sessionID)
|
||||
if (store.message[sessionID] === undefined) return false
|
||||
if (meta.limit[sessionID] === undefined) return false
|
||||
if (meta.complete[sessionID]) return false
|
||||
if (meta.limit[key] === undefined) return false
|
||||
if (meta.complete[key]) return false
|
||||
return true
|
||||
},
|
||||
loading(sessionID: string) {
|
||||
return meta.loading[sessionID] ?? false
|
||||
const key = keyFor(sdk.directory, sessionID)
|
||||
return meta.loading[key] ?? false
|
||||
},
|
||||
async loadMore(sessionID: string, count = chunk) {
|
||||
if (meta.loading[sessionID]) return
|
||||
if (meta.complete[sessionID]) return
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
if (meta.loading[key]) return
|
||||
if (meta.complete[key]) return
|
||||
|
||||
const current = meta.limit[sessionID] ?? chunk
|
||||
await loadMessages(sessionID, current + count)
|
||||
const currentLimit = meta.limit[key] ?? chunk
|
||||
await loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit: currentLimit + count,
|
||||
})
|
||||
},
|
||||
},
|
||||
fetch: async (count = 10) => {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
setStore("limit", (x) => x + count)
|
||||
await sdk.client.session.list().then((x) => {
|
||||
await client.session.list().then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.slice()
|
||||
@@ -236,9 +297,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
})
|
||||
},
|
||||
more: createMemo(() => store.session.length >= store.limit),
|
||||
more: createMemo(() => current()[0].session.length >= current()[0].limit),
|
||||
archive: async (sessionID: string) => {
|
||||
await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
await client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
||||
@@ -249,7 +313,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
},
|
||||
absolute,
|
||||
get directory() {
|
||||
return store.path.directory
|
||||
return current()[0].path.directory
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
656
packages/app/src/i18n/ar.ts
Normal file
656
packages/app/src/i18n/ar.ts
Normal file
@@ -0,0 +1,656 @@
|
||||
export const dict = {
|
||||
"command.category.suggested": "مقترح",
|
||||
"command.category.view": "عرض",
|
||||
"command.category.project": "مشروع",
|
||||
"command.category.provider": "موفر",
|
||||
"command.category.server": "خادم",
|
||||
"command.category.session": "جلسة",
|
||||
"command.category.theme": "سمة",
|
||||
"command.category.language": "لغة",
|
||||
"command.category.file": "ملف",
|
||||
"command.category.terminal": "محطة طرفية",
|
||||
"command.category.model": "نموذج",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "وكيل",
|
||||
"command.category.permissions": "أذونات",
|
||||
"command.category.workspace": "مساحة عمل",
|
||||
"command.category.settings": "إعدادات",
|
||||
|
||||
"theme.scheme.system": "نظام",
|
||||
"theme.scheme.light": "فاتح",
|
||||
"theme.scheme.dark": "داكن",
|
||||
|
||||
"command.sidebar.toggle": "تبديل الشريط الجانبي",
|
||||
"command.project.open": "فتح مشروع",
|
||||
"command.provider.connect": "اتصال بموفر",
|
||||
"command.server.switch": "تبديل الخادم",
|
||||
"command.settings.open": "فتح الإعدادات",
|
||||
"command.session.previous": "الجلسة السابقة",
|
||||
"command.session.next": "الجلسة التالية",
|
||||
"command.session.archive": "أرشفة الجلسة",
|
||||
|
||||
"command.palette": "لوحة الأوامر",
|
||||
|
||||
"command.theme.cycle": "تغيير السمة",
|
||||
"command.theme.set": "استخدام السمة: {{theme}}",
|
||||
"command.theme.scheme.cycle": "تغيير مخطط الألوان",
|
||||
"command.theme.scheme.set": "استخدام مخطط الألوان: {{scheme}}",
|
||||
|
||||
"command.language.cycle": "تغيير اللغة",
|
||||
"command.language.set": "استخدام اللغة: {{language}}",
|
||||
|
||||
"command.session.new": "جلسة جديدة",
|
||||
"command.file.open": "فتح ملف",
|
||||
"command.file.open.description": "البحث في الملفات والأوامر",
|
||||
"command.terminal.toggle": "تبديل المحطة الطرفية",
|
||||
"command.review.toggle": "تبديل المراجعة",
|
||||
"command.terminal.new": "محطة طرفية جديدة",
|
||||
"command.terminal.new.description": "إنشاء علامة تبويب جديدة للمحطة الطرفية",
|
||||
"command.steps.toggle": "تبديل الخطوات",
|
||||
"command.steps.toggle.description": "إظهار أو إخفاء خطوات الرسالة الحالية",
|
||||
"command.message.previous": "الرسالة السابقة",
|
||||
"command.message.previous.description": "انتقل إلى رسالة المستخدم السابقة",
|
||||
"command.message.next": "الرسالة التالية",
|
||||
"command.message.next.description": "انتقل إلى رسالة المستخدم التالية",
|
||||
"command.model.choose": "اختيار نموذج",
|
||||
"command.model.choose.description": "حدد نموذجًا مختلفًا",
|
||||
"command.mcp.toggle": "تبديل MCPs",
|
||||
"command.mcp.toggle.description": "تبديل MCPs",
|
||||
"command.agent.cycle": "تغيير الوكيل",
|
||||
"command.agent.cycle.description": "التبديل إلى الوكيل التالي",
|
||||
"command.agent.cycle.reverse": "تغيير الوكيل للخلف",
|
||||
"command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق",
|
||||
"command.model.variant.cycle": "تغيير جهد التفكير",
|
||||
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
||||
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
||||
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
||||
"command.session.undo": "تراجع",
|
||||
"command.session.undo.description": "تراجع عن الرسالة الأخيرة",
|
||||
"command.session.redo": "إعادة",
|
||||
"command.session.redo.description": "إعادة الرسالة التي تم التراجع عنها",
|
||||
"command.session.compact": "ضغط الجلسة",
|
||||
"command.session.compact.description": "تلخيص الجلسة لتقليل حجم السياق",
|
||||
"command.session.fork": "تشعب من الرسالة",
|
||||
"command.session.fork.description": "إنشاء جلسة جديدة من رسالة سابقة",
|
||||
"command.session.share": "مشاركة الجلسة",
|
||||
"command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة",
|
||||
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
||||
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
||||
|
||||
"palette.search.placeholder": "البحث في الملفات والأوامر",
|
||||
"palette.empty": "لا توجد نتائج",
|
||||
"palette.group.commands": "الأوامر",
|
||||
"palette.group.files": "الملفات",
|
||||
|
||||
"dialog.provider.search.placeholder": "البحث عن موفرين",
|
||||
"dialog.provider.empty": "لم يتم العثور على موفرين",
|
||||
"dialog.provider.group.popular": "شائع",
|
||||
"dialog.provider.group.other": "آخر",
|
||||
"dialog.provider.tag.recommended": "موصى به",
|
||||
"dialog.provider.anthropic.note": "اتصل باستخدام Claude Pro/Max أو مفتاح API",
|
||||
"dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API",
|
||||
"dialog.provider.copilot.note": "اتصل باستخدام Copilot أو مفتاح API",
|
||||
|
||||
"dialog.model.select.title": "تحديد نموذج",
|
||||
"dialog.model.search.placeholder": "البحث عن نماذج",
|
||||
"dialog.model.empty": "لا توجد نتائج للنماذج",
|
||||
"dialog.model.manage": "إدارة النماذج",
|
||||
"dialog.model.manage.description": "تخصيص النماذج التي تظهر في محدد النماذج.",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين",
|
||||
|
||||
"dialog.provider.viewAll": "عرض جميع الموفرين",
|
||||
|
||||
"provider.connect.title": "اتصال {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max",
|
||||
"provider.connect.selectMethod": "حدد طريقة تسجيل الدخول لـ {{provider}}.",
|
||||
"provider.connect.method.apiKey": "مفتاح API",
|
||||
"provider.connect.status.inProgress": "جارٍ التفويض...",
|
||||
"provider.connect.status.waiting": "في انتظار التفويض...",
|
||||
"provider.connect.status.failed": "فشل التفويض: {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"أدخل مفتاح واجهة برمجة تطبيقات {{provider}} الخاص بك لتوصيل حسابك واستخدام نماذج {{provider}} في OpenCode.",
|
||||
"provider.connect.apiKey.label": "مفتاح واجهة برمجة تطبيقات {{provider}}",
|
||||
"provider.connect.apiKey.placeholder": "مفتاح API",
|
||||
"provider.connect.apiKey.required": "مفتاح API مطلوب",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"يمنحك OpenCode Zen الوصول إلى مجموعة مختارة من النماذج الموثوقة والمحسنة لوكلاء البرمجة.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"باستخدام مفتاح API واحد، ستحصل على إمكانية الوصول إلى نماذج مثل Claude و GPT و Gemini و GLM والمزيد.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "قم بزيارة ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " للحصول على مفتاح API الخاص بك.",
|
||||
"provider.connect.oauth.code.visit.prefix": "قم بزيارة ",
|
||||
"provider.connect.oauth.code.visit.link": "هذا الرابط",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
" للحصول على رمز التفويض الخاص بك لتوصيل حسابك واستخدام نماذج {{provider}} في OpenCode.",
|
||||
"provider.connect.oauth.code.label": "رمز تفويض {{method}}",
|
||||
"provider.connect.oauth.code.placeholder": "رمز التفويض",
|
||||
"provider.connect.oauth.code.required": "رمز التفويض مطلوب",
|
||||
"provider.connect.oauth.code.invalid": "رمز التفويض غير صالح",
|
||||
"provider.connect.oauth.auto.visit.prefix": "قم بزيارة ",
|
||||
"provider.connect.oauth.auto.visit.link": "هذا الرابط",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في OpenCode.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "رمز التأكيد",
|
||||
"provider.connect.toast.connected.title": "تم توصيل {{provider}}",
|
||||
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
|
||||
|
||||
"model.tag.free": "مجاني",
|
||||
"model.tag.latest": "الأحدث",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
"model.provider.openai": "OpenAI",
|
||||
"model.provider.google": "Google",
|
||||
"model.provider.xai": "xAI",
|
||||
"model.provider.meta": "Meta",
|
||||
"model.input.text": "نص",
|
||||
"model.input.image": "صورة",
|
||||
"model.input.audio": "صوت",
|
||||
"model.input.video": "فيديو",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "يسمح: {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "يسمح بالاستنتاج",
|
||||
"model.tooltip.reasoning.none": "بدون استنتاج",
|
||||
"model.tooltip.context": "حد السياق {{limit}}",
|
||||
|
||||
"common.search.placeholder": "بحث",
|
||||
"common.goBack": "رجوع",
|
||||
"common.loading": "جارٍ التحميل",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "إلغاء",
|
||||
"common.submit": "إرسال",
|
||||
"common.save": "حفظ",
|
||||
"common.saving": "جارٍ الحفظ...",
|
||||
"common.default": "افتراضي",
|
||||
"common.attachment": "مرفق",
|
||||
|
||||
"prompt.placeholder.shell": "أدخل أمر shell...",
|
||||
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc للخروج",
|
||||
|
||||
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
|
||||
"prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",
|
||||
"prompt.example.3": "إصلاح الاختبارات المعطلة",
|
||||
"prompt.example.4": "اشرح كيف تعمل المصادقة",
|
||||
"prompt.example.5": "البحث عن وإصلاح الثغرات الأمنية",
|
||||
"prompt.example.6": "إضافة اختبارات وحدة لخدمة المستخدم",
|
||||
"prompt.example.7": "إعادة هيكلة هذه الدالة لتكون أكثر قابلية للقراءة",
|
||||
"prompt.example.8": "ماذا يعني هذا الخطأ؟",
|
||||
"prompt.example.9": "ساعدني في تصحيح هذه المشكلة",
|
||||
"prompt.example.10": "توليد وثائق API",
|
||||
"prompt.example.11": "تحسين استعلامات قاعدة البيانات",
|
||||
"prompt.example.12": "إضافة التحقق من صحة الإدخال",
|
||||
"prompt.example.13": "إنشاء مكون جديد لـ...",
|
||||
"prompt.example.14": "كيف أقوم بنشر هذا المشروع؟",
|
||||
"prompt.example.15": "مراجعة الكود الخاص بي لأفضل الممارسات",
|
||||
"prompt.example.16": "إضافة معالجة الأخطاء لهذه الدالة",
|
||||
"prompt.example.17": "اشرح نمط regex هذا",
|
||||
"prompt.example.18": "تحويل هذا إلى TypeScript",
|
||||
"prompt.example.19": "إضافة تسجيل الدخول (logging) في جميع أنحاء قاعدة التعليمات البرمجية",
|
||||
"prompt.example.20": "ما هي التبعيات القديمة؟",
|
||||
"prompt.example.21": "ساعدني في كتابة برنامج نصي للهجرة",
|
||||
"prompt.example.22": "تنفيذ التخزين المؤقت لهذه النقطة النهائية",
|
||||
"prompt.example.23": "إضافة ترقيم الصفحات إلى هذه القائمة",
|
||||
"prompt.example.24": "إنشاء أمر CLI لـ...",
|
||||
"prompt.example.25": "كيف تعمل متغيرات البيئة هنا؟",
|
||||
|
||||
"prompt.popover.emptyResults": "لا توجد نتائج مطابقة",
|
||||
"prompt.popover.emptyCommands": "لا توجد أوامر مطابقة",
|
||||
"prompt.dropzone.label": "أفلت الصور أو ملفات PDF هنا",
|
||||
"prompt.slash.badge.custom": "مخصص",
|
||||
"prompt.context.active": "نشط",
|
||||
"prompt.context.includeActiveFile": "تضمين الملف النشط",
|
||||
"prompt.context.removeActiveFile": "إزالة الملف النشط من السياق",
|
||||
"prompt.context.removeFile": "إزالة الملف من السياق",
|
||||
"prompt.action.attachFile": "إرفاق ملف",
|
||||
"prompt.attachment.remove": "إزالة المرفق",
|
||||
"prompt.action.send": "إرسال",
|
||||
"prompt.action.stop": "توقف",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "لصق غير مدعوم",
|
||||
"prompt.toast.pasteUnsupported.description": "يمكن لصق الصور أو ملفات PDF فقط هنا.",
|
||||
"prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً",
|
||||
"prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "فشل إنشاء شجرة العمل",
|
||||
"prompt.toast.sessionCreateFailed.title": "فشل إنشاء الجلسة",
|
||||
"prompt.toast.shellSendFailed.title": "فشل إرسال أمر shell",
|
||||
"prompt.toast.commandSendFailed.title": "فشل إرسال الأمر",
|
||||
"prompt.toast.promptSendFailed.title": "فشل إرسال الموجه",
|
||||
|
||||
"dialog.mcp.title": "MCPs",
|
||||
"dialog.mcp.description": "{{enabled}} من {{total}} مفعل",
|
||||
"dialog.mcp.empty": "لم يتم تكوين MCPs",
|
||||
|
||||
"mcp.status.connected": "متصل",
|
||||
"mcp.status.failed": "فشل",
|
||||
"mcp.status.needs_auth": "يحتاج إلى مصادقة",
|
||||
"mcp.status.disabled": "معطل",
|
||||
|
||||
"dialog.fork.empty": "لا توجد رسائل للتفرع منها",
|
||||
|
||||
"dialog.directory.search.placeholder": "البحث في المجلدات",
|
||||
"dialog.directory.empty": "لم يتم العثور على مجلدات",
|
||||
|
||||
"dialog.server.title": "الخوادم",
|
||||
"dialog.server.description": "تبديل خادم OpenCode الذي يتصل به هذا التطبيق.",
|
||||
"dialog.server.search.placeholder": "البحث في الخوادم",
|
||||
"dialog.server.empty": "لا توجد خوادم بعد",
|
||||
"dialog.server.add.title": "إضافة خادم",
|
||||
"dialog.server.add.url": "عنوان URL للخادم",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "تعذر الاتصال بالخادم",
|
||||
"dialog.server.add.checking": "جارٍ التحقق...",
|
||||
"dialog.server.add.button": "إضافة",
|
||||
"dialog.server.default.title": "الخادم الافتراضي",
|
||||
"dialog.server.default.description":
|
||||
"الاتصال بهذا الخادم عند بدء تشغيل التطبيق بدلاً من بدء خادم محلي. يتطلب إعادة التشغيل.",
|
||||
"dialog.server.default.none": "لم يتم تحديد خادم",
|
||||
"dialog.server.default.set": "تعيين الخادم الحالي كافتراضي",
|
||||
"dialog.server.default.clear": "مسح",
|
||||
"dialog.server.action.remove": "إزالة الخادم",
|
||||
|
||||
"dialog.project.edit.title": "تحرير المشروع",
|
||||
"dialog.project.edit.name": "الاسم",
|
||||
"dialog.project.edit.icon": "أيقونة",
|
||||
"dialog.project.edit.icon.alt": "أيقونة المشروع",
|
||||
"dialog.project.edit.icon.hint": "انقر أو اسحب صورة",
|
||||
"dialog.project.edit.icon.recommended": "موصى به: 128x128px",
|
||||
"dialog.project.edit.color": "لون",
|
||||
"dialog.project.edit.color.select": "اختر لون {{color}}",
|
||||
|
||||
"context.breakdown.title": "تفصيل السياق",
|
||||
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال. يشمل "أخرى" تعريفات الأدوات والنفقات العامة.',
|
||||
"context.breakdown.system": "النظام",
|
||||
"context.breakdown.user": "المستخدم",
|
||||
"context.breakdown.assistant": "المساعد",
|
||||
"context.breakdown.tool": "استدعاءات الأداة",
|
||||
"context.breakdown.other": "أخرى",
|
||||
|
||||
"context.systemPrompt.title": "موجه النظام",
|
||||
"context.rawMessages.title": "الرسائل الخام",
|
||||
|
||||
"context.stats.session": "جلسة",
|
||||
"context.stats.messages": "رسائل",
|
||||
"context.stats.provider": "موفر",
|
||||
"context.stats.model": "نموذج",
|
||||
"context.stats.limit": "حد السياق",
|
||||
"context.stats.totalTokens": "إجمالي الرموز",
|
||||
"context.stats.usage": "استخدام",
|
||||
"context.stats.inputTokens": "رموز الإدخال",
|
||||
"context.stats.outputTokens": "رموز الإخراج",
|
||||
"context.stats.reasoningTokens": "رموز الاستنتاج",
|
||||
"context.stats.cacheTokens": "رموز التخزين المؤقت (قراءة/كتابة)",
|
||||
"context.stats.userMessages": "رسائل المستخدم",
|
||||
"context.stats.assistantMessages": "رسائل المساعد",
|
||||
"context.stats.totalCost": "التكلفة الإجمالية",
|
||||
"context.stats.sessionCreated": "تم إنشاء الجلسة",
|
||||
"context.stats.lastActivity": "آخر نشاط",
|
||||
|
||||
"context.usage.tokens": "رموز",
|
||||
"context.usage.usage": "استخدام",
|
||||
"context.usage.cost": "تكلفة",
|
||||
"context.usage.clickToView": "انقر لعرض السياق",
|
||||
"context.usage.view": "عرض استخدام السياق",
|
||||
|
||||
"language.en": "الإنجليزية",
|
||||
"language.zh": "الصينية (المبسطة)",
|
||||
"language.zht": "الصينية (التقليدية)",
|
||||
"language.ko": "الكورية",
|
||||
"language.de": "الألمانية",
|
||||
"language.es": "الإسبانية",
|
||||
"language.fr": "الفرنسية",
|
||||
"language.ja": "اليابانية",
|
||||
"language.da": "الدانماركية",
|
||||
"language.ru": "الروسية",
|
||||
"language.pl": "البولندية",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "النرويجية",
|
||||
"language.br": "البرتغالية (البرازيل)",
|
||||
|
||||
"toast.language.title": "لغة",
|
||||
"toast.language.description": "تم التبديل إلى {{language}}",
|
||||
|
||||
"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.model.none.title": "لم يتم تحديد نموذج",
|
||||
"toast.model.none.description": "قم بتوصيل موفر لتلخيص هذه الجلسة",
|
||||
|
||||
"toast.file.loadFailed.title": "فشل تحميل الملف",
|
||||
|
||||
"toast.session.share.copyFailed.title": "فشل نسخ عنوان URL إلى الحافظة",
|
||||
"toast.session.share.success.title": "تمت مشاركة الجلسة",
|
||||
"toast.session.share.success.description": "تم نسخ عنوان URL للمشاركة إلى الحافظة!",
|
||||
"toast.session.share.failed.title": "فشل مشاركة الجلسة",
|
||||
"toast.session.share.failed.description": "حدث خطأ أثناء مشاركة الجلسة",
|
||||
|
||||
"toast.session.unshare.success.title": "تم إلغاء مشاركة الجلسة",
|
||||
"toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!",
|
||||
"toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة",
|
||||
"toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة",
|
||||
|
||||
"toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}",
|
||||
|
||||
"toast.update.title": "تحديث متاح",
|
||||
"toast.update.description": "نسخة جديدة من OpenCode ({{version}}) متاحة الآن للتثبيت.",
|
||||
"toast.update.action.installRestart": "تثبيت وإعادة تشغيل",
|
||||
"toast.update.action.notYet": "ليس الآن",
|
||||
|
||||
"error.page.title": "حدث خطأ ما",
|
||||
"error.page.description": "حدث خطأ أثناء تحميل التطبيق.",
|
||||
"error.page.details.label": "تفاصيل الخطأ",
|
||||
"error.page.action.restart": "إعادة تشغيل",
|
||||
"error.page.action.checking": "جارٍ التحقق...",
|
||||
"error.page.action.checkUpdates": "التحقق من وجود تحديثات",
|
||||
"error.page.action.updateTo": "تحديث إلى {{version}}",
|
||||
"error.page.report.prefix": "يرجى الإبلاغ عن هذا الخطأ لفريق OpenCode",
|
||||
"error.page.report.discord": "على Discord",
|
||||
"error.page.version": "الإصدار: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟",
|
||||
|
||||
"error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟",
|
||||
|
||||
"error.chain.unknown": "خطأ غير معروف",
|
||||
"error.chain.causedBy": "بسبب:",
|
||||
"error.chain.apiError": "خطأ API",
|
||||
"error.chain.status": "الحالة: {{status}}",
|
||||
"error.chain.retryable": "قابل لإعادة المحاولة: {{retryable}}",
|
||||
"error.chain.responseBody": "نص الاستجابة:\n{{body}}",
|
||||
"error.chain.didYouMean": "هل كنت تعني: {{suggestions}}",
|
||||
"error.chain.modelNotFound": "النموذج غير موجود: {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "تحقق من أسماء الموفر/النموذج في التكوين (opencode.json)",
|
||||
"error.chain.mcpFailed": 'فشل خادم MCP "{{name}}". لاحظ أن OpenCode لا يدعم مصادقة MCP بعد.',
|
||||
"error.chain.providerAuthFailed": "فشلت مصادقة الموفر ({{provider}}): {{message}}",
|
||||
"error.chain.providerInitFailed": 'فشل تهيئة الموفر "{{provider}}". تحقق من بيانات الاعتماد والتكوين.',
|
||||
"error.chain.configJsonInvalid": "ملف التكوين في {{path}} ليس JSON(C) صالحًا",
|
||||
"error.chain.configJsonInvalidWithMessage": "ملف التكوين في {{path}} ليس JSON(C) صالحًا: {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'الدليل "{{dir}}" في {{path}} غير صالح. أعد تسمية الدليل إلى "{{suggestion}}" أو قم بإزالته. هذا خطأ مطبعي شائع.',
|
||||
"error.chain.configFrontmatterError": "فشل تحليل frontmatter في {{path}}:\n{{message}}",
|
||||
"error.chain.configInvalid": "ملف التكوين في {{path}} غير صالح",
|
||||
"error.chain.configInvalidWithMessage": "ملف التكوين في {{path}} غير صالح: {{message}}",
|
||||
|
||||
"notification.permission.title": "مطلوب إذن",
|
||||
"notification.permission.description": "{{sessionTitle}} في {{projectName}} يحتاج إلى إذن",
|
||||
"notification.question.title": "سؤال",
|
||||
"notification.question.description": "{{sessionTitle}} في {{projectName}} لديه سؤال",
|
||||
"notification.action.goToSession": "انتقل إلى الجلسة",
|
||||
|
||||
"notification.session.responseReady.title": "الاستجابة جاهزة",
|
||||
"notification.session.error.title": "خطأ في الجلسة",
|
||||
"notification.session.error.fallbackDescription": "حدث خطأ",
|
||||
|
||||
"home.recentProjects": "المشاريع الحديثة",
|
||||
"home.empty.title": "لا توجد مشاريع حديثة",
|
||||
"home.empty.description": "ابدأ بفتح مشروع محلي",
|
||||
|
||||
"session.tab.session": "جلسة",
|
||||
"session.tab.review": "مراجعة",
|
||||
"session.tab.context": "سياق",
|
||||
"session.panel.reviewAndFiles": "المراجعة والملفات",
|
||||
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
|
||||
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
|
||||
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
|
||||
"session.messages.renderEarlier": "عرض الرسائل السابقة",
|
||||
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
|
||||
"session.messages.loadEarlier": "تحميل الرسائل السابقة",
|
||||
"session.messages.loading": "جارٍ تحميل الرسائل...",
|
||||
"session.messages.jumpToLatest": "الانتقال إلى الأحدث",
|
||||
|
||||
"session.context.addToContext": "إضافة {{selection}} إلى السياق",
|
||||
|
||||
"session.new.worktree.main": "الفرع الرئيسي",
|
||||
"session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})",
|
||||
"session.new.worktree.create": "إنشاء شجرة عمل جديدة",
|
||||
"session.new.lastModified": "آخر تعديل",
|
||||
|
||||
"session.header.search.placeholder": "بحث {{project}}",
|
||||
"session.header.searchFiles": "بحث عن الملفات",
|
||||
|
||||
"session.share.popover.title": "نشر على الويب",
|
||||
"session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.",
|
||||
"session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.",
|
||||
"session.share.action.share": "مشاركة",
|
||||
"session.share.action.publish": "نشر",
|
||||
"session.share.action.publishing": "جارٍ النشر...",
|
||||
"session.share.action.unpublish": "إلغاء النشر",
|
||||
"session.share.action.unpublishing": "جارٍ إلغاء النشر...",
|
||||
"session.share.action.view": "عرض",
|
||||
"session.share.copy.copied": "تم النسخ",
|
||||
"session.share.copy.copyLink": "نسخ الرابط",
|
||||
|
||||
"lsp.tooltip.none": "لا توجد خوادم LSP",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "جارٍ تحميل الموجه...",
|
||||
"terminal.loading": "جارٍ تحميل المحطة الطرفية...",
|
||||
"terminal.title": "محطة طرفية",
|
||||
"terminal.title.numbered": "محطة طرفية {{number}}",
|
||||
"terminal.close": "إغلاق المحطة الطرفية",
|
||||
"terminal.connectionLost.title": "فقد الاتصال",
|
||||
"terminal.connectionLost.description": "انقطع اتصال المحطة الطرفية. يمكن أن يحدث هذا عند إعادة تشغيل الخادم.",
|
||||
|
||||
"common.closeTab": "إغلاق علامة التبويب",
|
||||
"common.dismiss": "رفض",
|
||||
"common.requestFailed": "فشل الطلب",
|
||||
"common.moreOptions": "مزيد من الخيارات",
|
||||
"common.learnMore": "اعرف المزيد",
|
||||
"common.rename": "إعادة تسمية",
|
||||
"common.reset": "إعادة تعيين",
|
||||
"common.archive": "أرشفة",
|
||||
"common.delete": "حذف",
|
||||
"common.close": "إغلاق",
|
||||
"common.edit": "تحرير",
|
||||
"common.loadMore": "تحميل المزيد",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "تبديل القائمة",
|
||||
"sidebar.nav.projectsAndSessions": "المشاريع والجلسات",
|
||||
"sidebar.settings": "الإعدادات",
|
||||
"sidebar.help": "مساعدة",
|
||||
"sidebar.workspaces.enable": "تمكين مساحات العمل",
|
||||
"sidebar.workspaces.disable": "تعطيل مساحات العمل",
|
||||
"sidebar.gettingStarted.title": "البدء",
|
||||
"sidebar.gettingStarted.line1": "يتضمن OpenCode نماذج مجانية حتى تتمكن من البدء فورًا.",
|
||||
"sidebar.gettingStarted.line2": "قم بتوصيل أي موفر لاستخدام النماذج، بما في ذلك Claude و GPT و Gemini وما إلى ذلك.",
|
||||
"sidebar.project.recentSessions": "الجلسات الحديثة",
|
||||
"sidebar.project.viewAllSessions": "عرض جميع الجلسات",
|
||||
|
||||
"settings.section.desktop": "سطح المكتب",
|
||||
"settings.tab.general": "عام",
|
||||
"settings.tab.shortcuts": "اختصارات",
|
||||
|
||||
"settings.general.section.appearance": "المظهر",
|
||||
"settings.general.section.notifications": "إشعارات النظام",
|
||||
"settings.general.section.sounds": "المؤثرات الصوتية",
|
||||
|
||||
"settings.general.row.language.title": "اللغة",
|
||||
"settings.general.row.language.description": "تغيير لغة العرض لـ OpenCode",
|
||||
"settings.general.row.appearance.title": "المظهر",
|
||||
"settings.general.row.appearance.description": "تخصيص كيفية ظهور OpenCode على جهازك",
|
||||
"settings.general.row.theme.title": "السمة",
|
||||
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
||||
"settings.general.row.font.title": "الخط",
|
||||
"settings.general.row.font.description": "تخصيص الخط الأحادي المستخدم في كتل التعليمات البرمجية",
|
||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||
"font.option.cascadiaCode": "Cascadia Code",
|
||||
"font.option.firaCode": "Fira Code",
|
||||
"font.option.hack": "Hack",
|
||||
"font.option.inconsolata": "Inconsolata",
|
||||
"font.option.intelOneMono": "Intel One Mono",
|
||||
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||
"font.option.mesloLgs": "Meslo LGS",
|
||||
"font.option.robotoMono": "Roboto Mono",
|
||||
"font.option.sourceCodePro": "Source Code Pro",
|
||||
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||
"sound.option.alert01": "تنبيه 01",
|
||||
"sound.option.alert02": "تنبيه 02",
|
||||
"sound.option.alert03": "تنبيه 03",
|
||||
"sound.option.alert04": "تنبيه 04",
|
||||
"sound.option.alert05": "تنبيه 05",
|
||||
"sound.option.alert06": "تنبيه 06",
|
||||
"sound.option.alert07": "تنبيه 07",
|
||||
"sound.option.alert08": "تنبيه 08",
|
||||
"sound.option.alert09": "تنبيه 09",
|
||||
"sound.option.alert10": "تنبيه 10",
|
||||
"sound.option.bipbop01": "بيب بوب 01",
|
||||
"sound.option.bipbop02": "بيب بوب 02",
|
||||
"sound.option.bipbop03": "بيب بوب 03",
|
||||
"sound.option.bipbop04": "بيب بوب 04",
|
||||
"sound.option.bipbop05": "بيب بوب 05",
|
||||
"sound.option.bipbop06": "بيب بوب 06",
|
||||
"sound.option.bipbop07": "بيب بوب 07",
|
||||
"sound.option.bipbop08": "بيب بوب 08",
|
||||
"sound.option.bipbop09": "بيب بوب 09",
|
||||
"sound.option.bipbop10": "بيب بوب 10",
|
||||
"sound.option.staplebops01": "ستابل بوبس 01",
|
||||
"sound.option.staplebops02": "ستابل بوبس 02",
|
||||
"sound.option.staplebops03": "ستابل بوبس 03",
|
||||
"sound.option.staplebops04": "ستابل بوبس 04",
|
||||
"sound.option.staplebops05": "ستابل بوبس 05",
|
||||
"sound.option.staplebops06": "ستابل بوبس 06",
|
||||
"sound.option.staplebops07": "ستابل بوبس 07",
|
||||
"sound.option.nope01": "كلا 01",
|
||||
"sound.option.nope02": "كلا 02",
|
||||
"sound.option.nope03": "كلا 03",
|
||||
"sound.option.nope04": "كلا 04",
|
||||
"sound.option.nope05": "كلا 05",
|
||||
"sound.option.nope06": "كلا 06",
|
||||
"sound.option.nope07": "كلا 07",
|
||||
"sound.option.nope08": "كلا 08",
|
||||
"sound.option.nope09": "كلا 09",
|
||||
"sound.option.nope10": "كلا 10",
|
||||
"sound.option.nope11": "كلا 11",
|
||||
"sound.option.nope12": "كلا 12",
|
||||
"sound.option.yup01": "نعم 01",
|
||||
"sound.option.yup02": "نعم 02",
|
||||
"sound.option.yup03": "نعم 03",
|
||||
"sound.option.yup04": "نعم 04",
|
||||
"sound.option.yup05": "نعم 05",
|
||||
"sound.option.yup06": "نعم 06",
|
||||
|
||||
"settings.general.notifications.agent.title": "وكيل",
|
||||
"settings.general.notifications.agent.description": "عرض إشعار النظام عندما يكتمل الوكيل أو يحتاج إلى اهتمام",
|
||||
"settings.general.notifications.permissions.title": "أذونات",
|
||||
"settings.general.notifications.permissions.description": "عرض إشعار النظام عند الحاجة إلى إذن",
|
||||
"settings.general.notifications.errors.title": "أخطاء",
|
||||
"settings.general.notifications.errors.description": "عرض إشعار النظام عند حدوث خطأ",
|
||||
|
||||
"settings.general.sounds.agent.title": "وكيل",
|
||||
"settings.general.sounds.agent.description": "تشغيل صوت عندما يكتمل الوكيل أو يحتاج إلى اهتمام",
|
||||
"settings.general.sounds.permissions.title": "أذونات",
|
||||
"settings.general.sounds.permissions.description": "تشغيل صوت عند الحاجة إلى إذن",
|
||||
"settings.general.sounds.errors.title": "أخطاء",
|
||||
"settings.general.sounds.errors.description": "تشغيل صوت عند حدوث خطأ",
|
||||
|
||||
"settings.shortcuts.title": "اختصارات لوحة المفاتيح",
|
||||
"settings.shortcuts.reset.button": "إعادة التعيين إلى الافتراضيات",
|
||||
"settings.shortcuts.reset.toast.title": "تم إعادة تعيين الاختصارات",
|
||||
"settings.shortcuts.reset.toast.description": "تم إعادة تعيين اختصارات لوحة المفاتيح إلى الافتراضيات.",
|
||||
"settings.shortcuts.conflict.title": "الاختصار قيد الاستخدام بالفعل",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} معين بالفعل لـ {{titles}}.",
|
||||
"settings.shortcuts.unassigned": "غير معين",
|
||||
"settings.shortcuts.pressKeys": "اضغط على المفاتيح",
|
||||
"settings.shortcuts.search.placeholder": "البحث في الاختصارات",
|
||||
"settings.shortcuts.search.empty": "لم يتم العثور على اختصارات",
|
||||
|
||||
"settings.shortcuts.group.general": "عام",
|
||||
"settings.shortcuts.group.session": "جلسة",
|
||||
"settings.shortcuts.group.navigation": "تصفح",
|
||||
"settings.shortcuts.group.modelAndAgent": "النموذج والوكيل",
|
||||
"settings.shortcuts.group.terminal": "المحطة الطرفية",
|
||||
"settings.shortcuts.group.prompt": "موجه",
|
||||
|
||||
"settings.providers.title": "الموفرون",
|
||||
"settings.providers.description": "ستكون إعدادات الموفر قابلة للتكوين هنا.",
|
||||
"settings.models.title": "النماذج",
|
||||
"settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.",
|
||||
"settings.agents.title": "الوكلاء",
|
||||
"settings.agents.description": "ستكون إعدادات الوكيل قابلة للتكوين هنا.",
|
||||
"settings.commands.title": "الأوامر",
|
||||
"settings.commands.description": "ستكون إعدادات الأمر قابلة للتكوين هنا.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "ستكون إعدادات MCP قابلة للتكوين هنا.",
|
||||
|
||||
"settings.permissions.title": "الأذونات",
|
||||
"settings.permissions.description": "تحكم في الأدوات التي يمكن للخادم استخدامها بشكل افتراضي.",
|
||||
"settings.permissions.section.tools": "الأدوات",
|
||||
"settings.permissions.toast.updateFailed.title": "فشل تحديث الأذونات",
|
||||
|
||||
"settings.permissions.action.allow": "سماح",
|
||||
"settings.permissions.action.ask": "سؤال",
|
||||
"settings.permissions.action.deny": "رفض",
|
||||
|
||||
"settings.permissions.tool.read.title": "قراءة",
|
||||
"settings.permissions.tool.read.description": "قراءة ملف (يطابق مسار الملف)",
|
||||
"settings.permissions.tool.edit.title": "تحرير",
|
||||
"settings.permissions.tool.edit.description":
|
||||
"تعديل الملفات، بما في ذلك التحرير والكتابة والتصحيحات والتحرير المتعدد",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "مطابقة الملفات باستخدام أنماط glob",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description": "البحث في محتويات الملف باستخدام التعبيرات العادية",
|
||||
"settings.permissions.tool.list.title": "قائمة",
|
||||
"settings.permissions.tool.list.description": "سرد الملفات داخل دليل",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "تشغيل أوامر shell",
|
||||
"settings.permissions.tool.task.title": "Task",
|
||||
"settings.permissions.tool.task.description": "تشغيل الوكلاء الفرعيين",
|
||||
"settings.permissions.tool.skill.title": "Skill",
|
||||
"settings.permissions.tool.skill.description": "تحميل مهارة بالاسم",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة",
|
||||
"settings.permissions.tool.todoread.title": "قراءة المهام",
|
||||
"settings.permissions.tool.todoread.description": "قراءة قائمة المهام",
|
||||
"settings.permissions.tool.todowrite.title": "كتابة المهام",
|
||||
"settings.permissions.tool.todowrite.description": "تحديث قائمة المهام",
|
||||
"settings.permissions.tool.webfetch.title": "جلب الويب",
|
||||
"settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL",
|
||||
"settings.permissions.tool.websearch.title": "بحث الويب",
|
||||
"settings.permissions.tool.websearch.description": "البحث في الويب",
|
||||
"settings.permissions.tool.codesearch.title": "بحث الكود",
|
||||
"settings.permissions.tool.codesearch.description": "البحث عن كود على الويب",
|
||||
"settings.permissions.tool.external_directory.title": "دليل خارجي",
|
||||
"settings.permissions.tool.external_directory.description": "الوصول إلى الملفات خارج دليل المشروع",
|
||||
"settings.permissions.tool.doom_loop.title": "حلقة الموت",
|
||||
"settings.permissions.tool.doom_loop.description": "اكتشاف استدعاءات الأدوات المتكررة بمدخلات متطابقة",
|
||||
|
||||
"session.delete.failed.title": "فشل حذف الجلسة",
|
||||
"session.delete.title": "حذف الجلسة",
|
||||
"session.delete.confirm": 'حذف الجلسة "{{name}}"؟',
|
||||
"session.delete.button": "حذف الجلسة",
|
||||
|
||||
"workspace.new": "مساحة عمل جديدة",
|
||||
"workspace.type.local": "محلي",
|
||||
"workspace.type.sandbox": "صندوق رمل",
|
||||
"workspace.create.failed.title": "فشل إنشاء مساحة العمل",
|
||||
"workspace.delete.failed.title": "فشل حذف مساحة العمل",
|
||||
"workspace.resetting.title": "إعادة تعيين مساحة العمل",
|
||||
"workspace.resetting.description": "قد يستغرق هذا دقيقة.",
|
||||
"workspace.reset.failed.title": "فشل إعادة تعيين مساحة العمل",
|
||||
"workspace.reset.success.title": "تمت إعادة تعيين مساحة العمل",
|
||||
"workspace.reset.success.description": "مساحة العمل تطابق الآن الفرع الافتراضي.",
|
||||
"workspace.status.checking": "التحقق من التغييرات غير المدمجة...",
|
||||
"workspace.status.error": "تعذر التحقق من حالة git.",
|
||||
"workspace.status.clean": "لم يتم اكتشاف تغييرات غير مدمجة.",
|
||||
"workspace.status.dirty": "تم اكتشاف تغييرات غير مدمجة في مساحة العمل هذه.",
|
||||
"workspace.delete.title": "حذف مساحة العمل",
|
||||
"workspace.delete.confirm": 'حذف مساحة العمل "{{name}}"؟',
|
||||
"workspace.delete.button": "حذف مساحة العمل",
|
||||
"workspace.reset.title": "إعادة تعيين مساحة العمل",
|
||||
"workspace.reset.confirm": 'إعادة تعيين مساحة العمل "{{name}}"؟',
|
||||
"workspace.reset.button": "إعادة تعيين مساحة العمل",
|
||||
"workspace.reset.archived.none": "لن تتم أرشفة أي جلسات نشطة.",
|
||||
"workspace.reset.archived.one": "ستتم أرشفة جلسة واحدة.",
|
||||
"workspace.reset.archived.many": "ستتم أرشفة {{count}} جلسات.",
|
||||
"workspace.reset.note": "سيؤدي هذا إلى إعادة تعيين مساحة العمل لتتطابق مع الفرع الافتراضي.",
|
||||
}
|
||||
667
packages/app/src/i18n/br.ts
Normal file
667
packages/app/src/i18n/br.ts
Normal file
@@ -0,0 +1,667 @@
|
||||
export const dict = {
|
||||
"command.category.suggested": "Sugerido",
|
||||
"command.category.view": "Visualizar",
|
||||
"command.category.project": "Projeto",
|
||||
"command.category.provider": "Provedor",
|
||||
"command.category.server": "Servidor",
|
||||
"command.category.session": "Sessão",
|
||||
"command.category.theme": "Tema",
|
||||
"command.category.language": "Idioma",
|
||||
"command.category.file": "Arquivo",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modelo",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "Agente",
|
||||
"command.category.permissions": "Permissões",
|
||||
"command.category.workspace": "Espaço de trabalho",
|
||||
"command.category.settings": "Configurações",
|
||||
|
||||
"theme.scheme.system": "Sistema",
|
||||
"theme.scheme.light": "Claro",
|
||||
"theme.scheme.dark": "Escuro",
|
||||
|
||||
"command.sidebar.toggle": "Alternar barra lateral",
|
||||
"command.project.open": "Abrir projeto",
|
||||
"command.provider.connect": "Conectar provedor",
|
||||
"command.server.switch": "Trocar servidor",
|
||||
"command.settings.open": "Abrir configurações",
|
||||
"command.session.previous": "Sessão anterior",
|
||||
"command.session.next": "Próxima sessão",
|
||||
"command.session.archive": "Arquivar sessão",
|
||||
|
||||
"command.palette": "Paleta de comandos",
|
||||
|
||||
"command.theme.cycle": "Alternar tema",
|
||||
"command.theme.set": "Usar tema: {{theme}}",
|
||||
"command.theme.scheme.cycle": "Alternar esquema de cores",
|
||||
"command.theme.scheme.set": "Usar esquema de cores: {{scheme}}",
|
||||
|
||||
"command.language.cycle": "Alternar idioma",
|
||||
"command.language.set": "Usar idioma: {{language}}",
|
||||
|
||||
"command.session.new": "Nova sessão",
|
||||
"command.file.open": "Abrir arquivo",
|
||||
"command.file.open.description": "Buscar arquivos e comandos",
|
||||
"command.terminal.toggle": "Alternar terminal",
|
||||
"command.review.toggle": "Alternar revisão",
|
||||
"command.terminal.new": "Novo terminal",
|
||||
"command.terminal.new.description": "Criar uma nova aba de terminal",
|
||||
"command.steps.toggle": "Alternar passos",
|
||||
"command.steps.toggle.description": "Mostrar ou ocultar passos da mensagem atual",
|
||||
"command.message.previous": "Mensagem anterior",
|
||||
"command.message.previous.description": "Ir para a mensagem de usuário anterior",
|
||||
"command.message.next": "Próxima mensagem",
|
||||
"command.message.next.description": "Ir para a próxima mensagem de usuário",
|
||||
"command.model.choose": "Escolher modelo",
|
||||
"command.model.choose.description": "Selecionar um modelo diferente",
|
||||
"command.mcp.toggle": "Alternar MCPs",
|
||||
"command.mcp.toggle.description": "Alternar MCPs",
|
||||
"command.agent.cycle": "Alternar agente",
|
||||
"command.agent.cycle.description": "Mudar para o próximo agente",
|
||||
"command.agent.cycle.reverse": "Alternar agente (reverso)",
|
||||
"command.agent.cycle.reverse.description": "Mudar para o agente anterior",
|
||||
"command.model.variant.cycle": "Alternar nível de raciocínio",
|
||||
"command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
|
||||
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
|
||||
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
|
||||
"command.session.undo": "Desfazer",
|
||||
"command.session.undo.description": "Desfazer a última mensagem",
|
||||
"command.session.redo": "Refazer",
|
||||
"command.session.redo.description": "Refazer a última mensagem desfeita",
|
||||
"command.session.compact": "Compactar sessão",
|
||||
"command.session.compact.description": "Resumir a sessão para reduzir o tamanho do contexto",
|
||||
"command.session.fork": "Bifurcar da mensagem",
|
||||
"command.session.fork.description": "Criar uma nova sessão a partir de uma mensagem anterior",
|
||||
"command.session.share": "Compartilhar sessão",
|
||||
"command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência",
|
||||
"command.session.unshare": "Parar de compartilhar sessão",
|
||||
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
||||
|
||||
"palette.search.placeholder": "Buscar arquivos e comandos",
|
||||
"palette.empty": "Nenhum resultado encontrado",
|
||||
"palette.group.commands": "Comandos",
|
||||
"palette.group.files": "Arquivos",
|
||||
|
||||
"dialog.provider.search.placeholder": "Buscar provedores",
|
||||
"dialog.provider.empty": "Nenhum provedor encontrado",
|
||||
"dialog.provider.group.popular": "Popular",
|
||||
"dialog.provider.group.other": "Outro",
|
||||
"dialog.provider.tag.recommended": "Recomendado",
|
||||
"dialog.provider.anthropic.note": "Conectar com Claude Pro/Max ou chave de API",
|
||||
"dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API",
|
||||
"dialog.provider.copilot.note": "Conectar com Copilot ou chave de API",
|
||||
|
||||
"dialog.model.select.title": "Selecionar modelo",
|
||||
"dialog.model.search.placeholder": "Buscar modelos",
|
||||
"dialog.model.empty": "Nenhum resultado de modelo",
|
||||
"dialog.model.manage": "Gerenciar modelos",
|
||||
"dialog.model.manage.description": "Personalizar quais modelos aparecem no seletor de modelos.",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares",
|
||||
|
||||
"dialog.provider.viewAll": "Ver todos os provedores",
|
||||
|
||||
"provider.connect.title": "Conectar {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max",
|
||||
"provider.connect.selectMethod": "Selecionar método de login para {{provider}}.",
|
||||
"provider.connect.method.apiKey": "Chave de API",
|
||||
"provider.connect.status.inProgress": "Autorização em andamento...",
|
||||
"provider.connect.status.waiting": "Aguardando autorização...",
|
||||
"provider.connect.status.failed": "Autorização falhou: {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"Digite sua chave de API do {{provider}} para conectar sua conta e usar modelos do {{provider}} no OpenCode.",
|
||||
"provider.connect.apiKey.label": "Chave de API do {{provider}}",
|
||||
"provider.connect.apiKey.placeholder": "Chave de API",
|
||||
"provider.connect.apiKey.required": "A chave de API é obrigatória",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"OpenCode Zen oferece acesso a um conjunto selecionado de modelos confiáveis otimizados para agentes de código.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Com uma única chave de API você terá acesso a modelos como Claude, GPT, Gemini, GLM e mais.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Visite ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " para obter sua chave de API.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Visite ",
|
||||
"provider.connect.oauth.code.visit.link": "este link",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
" para obter seu código de autorização e conectar sua conta para usar modelos do {{provider}} no OpenCode.",
|
||||
"provider.connect.oauth.code.label": "Código de autorização {{method}}",
|
||||
"provider.connect.oauth.code.placeholder": "Código de autorização",
|
||||
"provider.connect.oauth.code.required": "O código de autorização é obrigatório",
|
||||
"provider.connect.oauth.code.invalid": "Código de autorização inválido",
|
||||
"provider.connect.oauth.auto.visit.prefix": "Visite ",
|
||||
"provider.connect.oauth.auto.visit.link": "este link",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" e digite o código abaixo para conectar sua conta e usar modelos do {{provider}} no OpenCode.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "Código de confirmação",
|
||||
"provider.connect.toast.connected.title": "{{provider}} conectado",
|
||||
"provider.connect.toast.connected.description": "Modelos do {{provider}} agora estão disponíveis para uso.",
|
||||
|
||||
"model.tag.free": "Grátis",
|
||||
"model.tag.latest": "Mais recente",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
"model.provider.openai": "OpenAI",
|
||||
"model.provider.google": "Google",
|
||||
"model.provider.xai": "xAI",
|
||||
"model.provider.meta": "Meta",
|
||||
"model.input.text": "texto",
|
||||
"model.input.image": "imagem",
|
||||
"model.input.audio": "áudio",
|
||||
"model.input.video": "vídeo",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Permite: {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "Permite raciocínio",
|
||||
"model.tooltip.reasoning.none": "Sem raciocínio",
|
||||
"model.tooltip.context": "Limite de contexto {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Buscar",
|
||||
"common.goBack": "Voltar",
|
||||
"common.loading": "Carregando",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.submit": "Enviar",
|
||||
"common.save": "Salvar",
|
||||
"common.saving": "Salvando...",
|
||||
"common.default": "Padrão",
|
||||
"common.attachment": "anexo",
|
||||
|
||||
"prompt.placeholder.shell": "Digite comando do shell...",
|
||||
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc para sair",
|
||||
|
||||
"prompt.example.1": "Corrigir um TODO no código",
|
||||
"prompt.example.2": "Qual é a stack tecnológica deste projeto?",
|
||||
"prompt.example.3": "Corrigir testes quebrados",
|
||||
"prompt.example.4": "Explicar como funciona a autenticação",
|
||||
"prompt.example.5": "Encontrar e corrigir vulnerabilidades de segurança",
|
||||
"prompt.example.6": "Adicionar testes unitários para o serviço de usuário",
|
||||
"prompt.example.7": "Refatorar esta função para melhor legibilidade",
|
||||
"prompt.example.8": "O que significa este erro?",
|
||||
"prompt.example.9": "Me ajude a depurar este problema",
|
||||
"prompt.example.10": "Gerar documentação da API",
|
||||
"prompt.example.11": "Otimizar consultas ao banco de dados",
|
||||
"prompt.example.12": "Adicionar validação de entrada",
|
||||
"prompt.example.13": "Criar um novo componente para...",
|
||||
"prompt.example.14": "Como faço o deploy deste projeto?",
|
||||
"prompt.example.15": "Revisar meu código para boas práticas",
|
||||
"prompt.example.16": "Adicionar tratamento de erros a esta função",
|
||||
"prompt.example.17": "Explicar este padrão regex",
|
||||
"prompt.example.18": "Converter isto para TypeScript",
|
||||
"prompt.example.19": "Adicionar logging em todo o código",
|
||||
"prompt.example.20": "Quais dependências estão desatualizadas?",
|
||||
"prompt.example.21": "Me ajude a escrever um script de migração",
|
||||
"prompt.example.22": "Implementar cache para este endpoint",
|
||||
"prompt.example.23": "Adicionar paginação a esta lista",
|
||||
"prompt.example.24": "Criar um comando CLI para...",
|
||||
"prompt.example.25": "Como funcionam as variáveis de ambiente aqui?",
|
||||
|
||||
"prompt.popover.emptyResults": "Nenhum resultado correspondente",
|
||||
"prompt.popover.emptyCommands": "Nenhum comando correspondente",
|
||||
"prompt.dropzone.label": "Solte imagens ou PDFs aqui",
|
||||
"prompt.slash.badge.custom": "personalizado",
|
||||
"prompt.context.active": "ativo",
|
||||
"prompt.context.includeActiveFile": "Incluir arquivo ativo",
|
||||
"prompt.context.removeActiveFile": "Remover arquivo ativo do contexto",
|
||||
"prompt.context.removeFile": "Remover arquivo do contexto",
|
||||
"prompt.action.attachFile": "Anexar arquivo",
|
||||
"prompt.attachment.remove": "Remover anexo",
|
||||
"prompt.action.send": "Enviar",
|
||||
"prompt.action.stop": "Parar",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Colagem não suportada",
|
||||
"prompt.toast.pasteUnsupported.description": "Somente imagens ou PDFs podem ser colados aqui.",
|
||||
"prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo",
|
||||
"prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Falha ao criar worktree",
|
||||
"prompt.toast.sessionCreateFailed.title": "Falha ao criar sessão",
|
||||
"prompt.toast.shellSendFailed.title": "Falha ao enviar comando shell",
|
||||
"prompt.toast.commandSendFailed.title": "Falha ao enviar comando",
|
||||
"prompt.toast.promptSendFailed.title": "Falha ao enviar prompt",
|
||||
|
||||
"dialog.mcp.title": "MCPs",
|
||||
"dialog.mcp.description": "{{enabled}} de {{total}} habilitados",
|
||||
"dialog.mcp.empty": "Nenhum MCP configurado",
|
||||
|
||||
"mcp.status.connected": "conectado",
|
||||
"mcp.status.failed": "falhou",
|
||||
"mcp.status.needs_auth": "precisa de autenticação",
|
||||
"mcp.status.disabled": "desabilitado",
|
||||
|
||||
"dialog.fork.empty": "Nenhuma mensagem para bifurcar",
|
||||
|
||||
"dialog.directory.search.placeholder": "Buscar pastas",
|
||||
"dialog.directory.empty": "Nenhuma pasta encontrada",
|
||||
|
||||
"dialog.server.title": "Servidores",
|
||||
"dialog.server.description": "Trocar para qual servidor OpenCode este aplicativo se conecta.",
|
||||
"dialog.server.search.placeholder": "Buscar servidores",
|
||||
"dialog.server.empty": "Nenhum servidor ainda",
|
||||
"dialog.server.add.title": "Adicionar um servidor",
|
||||
"dialog.server.add.url": "URL do servidor",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "Não foi possível conectar ao servidor",
|
||||
"dialog.server.add.checking": "Verificando...",
|
||||
"dialog.server.add.button": "Adicionar",
|
||||
"dialog.server.default.title": "Servidor padrão",
|
||||
"dialog.server.default.description":
|
||||
"Conectar a este servidor na inicialização do aplicativo ao invés de iniciar um servidor local. Requer reinicialização.",
|
||||
"dialog.server.default.none": "Nenhum servidor selecionado",
|
||||
"dialog.server.default.set": "Definir servidor atual como padrão",
|
||||
"dialog.server.default.clear": "Limpar",
|
||||
"dialog.server.action.remove": "Remover servidor",
|
||||
|
||||
"dialog.project.edit.title": "Editar projeto",
|
||||
"dialog.project.edit.name": "Nome",
|
||||
"dialog.project.edit.icon": "Ícone",
|
||||
"dialog.project.edit.icon.alt": "Ícone do projeto",
|
||||
"dialog.project.edit.icon.hint": "Clique ou arraste uma imagem",
|
||||
"dialog.project.edit.icon.recommended": "Recomendado: 128x128px",
|
||||
"dialog.project.edit.color": "Cor",
|
||||
"dialog.project.edit.color.select": "Selecionar cor {{color}}",
|
||||
"dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho",
|
||||
"dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "ex: bun install",
|
||||
|
||||
"context.breakdown.title": "Detalhamento do Contexto",
|
||||
"context.breakdown.note":
|
||||
'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e overhead.',
|
||||
"context.breakdown.system": "Sistema",
|
||||
"context.breakdown.user": "Usuário",
|
||||
"context.breakdown.assistant": "Assistente",
|
||||
"context.breakdown.tool": "Chamadas de Ferramentas",
|
||||
"context.breakdown.other": "Outros",
|
||||
|
||||
"context.systemPrompt.title": "Prompt do Sistema",
|
||||
"context.rawMessages.title": "Mensagens brutas",
|
||||
|
||||
"context.stats.session": "Sessão",
|
||||
"context.stats.messages": "Mensagens",
|
||||
"context.stats.provider": "Provedor",
|
||||
"context.stats.model": "Modelo",
|
||||
"context.stats.limit": "Limite de Contexto",
|
||||
"context.stats.totalTokens": "Total de Tokens",
|
||||
"context.stats.usage": "Uso",
|
||||
"context.stats.inputTokens": "Tokens de Entrada",
|
||||
"context.stats.outputTokens": "Tokens de Saída",
|
||||
"context.stats.reasoningTokens": "Tokens de Raciocínio",
|
||||
"context.stats.cacheTokens": "Tokens de Cache (leitura/escrita)",
|
||||
"context.stats.userMessages": "Mensagens de Usuário",
|
||||
"context.stats.assistantMessages": "Mensagens do Assistente",
|
||||
"context.stats.totalCost": "Custo Total",
|
||||
"context.stats.sessionCreated": "Sessão Criada",
|
||||
"context.stats.lastActivity": "Última Atividade",
|
||||
|
||||
"context.usage.tokens": "Tokens",
|
||||
"context.usage.usage": "Uso",
|
||||
"context.usage.cost": "Custo",
|
||||
"context.usage.clickToView": "Clique para ver o contexto",
|
||||
"context.usage.view": "Ver uso do contexto",
|
||||
|
||||
"language.en": "Inglês",
|
||||
"language.zh": "Chinês (Simplificado)",
|
||||
"language.zht": "Chinês (Tradicional)",
|
||||
"language.ko": "Coreano",
|
||||
"language.de": "Alemão",
|
||||
"language.es": "Espanhol",
|
||||
"language.fr": "Francês",
|
||||
"language.ja": "Japonês",
|
||||
"language.da": "Dinamarquês",
|
||||
"language.ru": "Russo",
|
||||
"language.pl": "Polonês",
|
||||
"language.ar": "Árabe",
|
||||
"language.no": "Norueguês",
|
||||
"language.br": "Português (Brasil)",
|
||||
|
||||
"toast.language.title": "Idioma",
|
||||
"toast.language.description": "Alterado para {{language}}",
|
||||
|
||||
"toast.theme.title": "Tema alterado",
|
||||
"toast.scheme.title": "Esquema de cores",
|
||||
|
||||
"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.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",
|
||||
|
||||
"toast.session.share.copyFailed.title": "Falha ao copiar URL para a área de transferência",
|
||||
"toast.session.share.success.title": "Sessão compartilhada",
|
||||
"toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!",
|
||||
"toast.session.share.failed.title": "Falha ao compartilhar sessão",
|
||||
"toast.session.share.failed.description": "Ocorreu um erro ao compartilhar a sessão",
|
||||
|
||||
"toast.session.unshare.success.title": "Sessão não compartilhada",
|
||||
"toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!",
|
||||
"toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão",
|
||||
"toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão",
|
||||
|
||||
"toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}",
|
||||
|
||||
"toast.update.title": "Atualização disponível",
|
||||
"toast.update.description": "Uma nova versão do OpenCode ({{version}}) está disponível para instalação.",
|
||||
"toast.update.action.installRestart": "Instalar e reiniciar",
|
||||
"toast.update.action.notYet": "Agora não",
|
||||
|
||||
"error.page.title": "Algo deu errado",
|
||||
"error.page.description": "Ocorreu um erro ao carregar a aplicação.",
|
||||
"error.page.details.label": "Detalhes do Erro",
|
||||
"error.page.action.restart": "Reiniciar",
|
||||
"error.page.action.checking": "Verificando...",
|
||||
"error.page.action.checkUpdates": "Verificar atualizações",
|
||||
"error.page.action.updateTo": "Atualizar para {{version}}",
|
||||
"error.page.report.prefix": "Por favor, reporte este erro para a equipe do OpenCode",
|
||||
"error.page.report.discord": "no Discord",
|
||||
"error.page.version": "Versão: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?",
|
||||
|
||||
"error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?",
|
||||
|
||||
"error.chain.unknown": "Erro desconhecido",
|
||||
"error.chain.causedBy": "Causado por:",
|
||||
"error.chain.apiError": "Erro de API",
|
||||
"error.chain.status": "Status: {{status}}",
|
||||
"error.chain.retryable": "Pode tentar novamente: {{retryable}}",
|
||||
"error.chain.responseBody": "Corpo da resposta:\n{{body}}",
|
||||
"error.chain.didYouMean": "Você quis dizer: {{suggestions}}",
|
||||
"error.chain.modelNotFound": "Modelo não encontrado: {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "Verifique os nomes de provedor/modelo na sua configuração (opencode.json)",
|
||||
"error.chain.mcpFailed": 'Servidor MCP "{{name}}" falhou. Nota: OpenCode ainda não suporta autenticação MCP.',
|
||||
"error.chain.providerAuthFailed": "Autenticação do provedor falhou ({{provider}}): {{message}}",
|
||||
"error.chain.providerInitFailed":
|
||||
'Falha ao inicializar provedor "{{provider}}". Verifique credenciais e configuração.',
|
||||
"error.chain.configJsonInvalid": "Arquivo de configuração em {{path}} não é um JSON(C) válido",
|
||||
"error.chain.configJsonInvalidWithMessage":
|
||||
"Arquivo de configuração em {{path}} não é um JSON(C) válido: {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'Diretório "{{dir}}" em {{path}} não é válido. Renomeie o diretório para "{{suggestion}}" ou remova-o. Este é um erro de digitação comum.',
|
||||
"error.chain.configFrontmatterError": "Falha ao analisar frontmatter em {{path}}:\n{{message}}",
|
||||
"error.chain.configInvalid": "Arquivo de configuração em {{path}} é inválido",
|
||||
"error.chain.configInvalidWithMessage": "Arquivo de configuração em {{path}} é inválido: {{message}}",
|
||||
|
||||
"notification.permission.title": "Permissão necessária",
|
||||
"notification.permission.description": "{{sessionTitle}} em {{projectName}} precisa de permissão",
|
||||
"notification.question.title": "Pergunta",
|
||||
"notification.question.description": "{{sessionTitle}} em {{projectName}} tem uma pergunta",
|
||||
"notification.action.goToSession": "Ir para sessão",
|
||||
|
||||
"notification.session.responseReady.title": "Resposta pronta",
|
||||
"notification.session.error.title": "Erro na sessão",
|
||||
"notification.session.error.fallbackDescription": "Ocorreu um erro",
|
||||
|
||||
"home.recentProjects": "Projetos recentes",
|
||||
"home.empty.title": "Nenhum projeto recente",
|
||||
"home.empty.description": "Comece abrindo um projeto local",
|
||||
|
||||
"session.tab.session": "Sessão",
|
||||
"session.tab.review": "Revisão",
|
||||
"session.tab.context": "Contexto",
|
||||
"session.panel.reviewAndFiles": "Revisão e arquivos",
|
||||
"session.review.filesChanged": "{{count}} Arquivos Alterados",
|
||||
"session.review.loadingChanges": "Carregando alterações...",
|
||||
"session.review.empty": "Nenhuma alteração nesta sessão ainda",
|
||||
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
|
||||
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
|
||||
"session.messages.loadEarlier": "Carregar mensagens anteriores",
|
||||
"session.messages.loading": "Carregando mensagens...",
|
||||
"session.messages.jumpToLatest": "Ir para a mais recente",
|
||||
|
||||
"session.context.addToContext": "Adicionar {{selection}} ao contexto",
|
||||
|
||||
"session.new.worktree.main": "Branch principal",
|
||||
"session.new.worktree.mainWithBranch": "Branch principal ({{branch}})",
|
||||
"session.new.worktree.create": "Criar novo worktree",
|
||||
"session.new.lastModified": "Última modificação",
|
||||
|
||||
"session.header.search.placeholder": "Buscar {{project}}",
|
||||
"session.header.searchFiles": "Buscar arquivos",
|
||||
|
||||
"session.share.popover.title": "Publicar na web",
|
||||
"session.share.popover.description.shared":
|
||||
"Esta sessão é pública na web. Está acessível para qualquer pessoa com o link.",
|
||||
"session.share.popover.description.unshared":
|
||||
"Compartilhar sessão publicamente na web. Estará acessível para qualquer pessoa com o link.",
|
||||
"session.share.action.share": "Compartilhar",
|
||||
"session.share.action.publish": "Publicar",
|
||||
"session.share.action.publishing": "Publicando...",
|
||||
"session.share.action.unpublish": "Cancelar publicação",
|
||||
"session.share.action.unpublishing": "Cancelando publicação...",
|
||||
"session.share.action.view": "Ver",
|
||||
"session.share.copy.copied": "Copiado",
|
||||
"session.share.copy.copyLink": "Copiar link",
|
||||
|
||||
"lsp.tooltip.none": "Nenhum servidor LSP",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "Carregando prompt...",
|
||||
"terminal.loading": "Carregando terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Fechar terminal",
|
||||
"terminal.connectionLost.title": "Conexão Perdida",
|
||||
"terminal.connectionLost.description":
|
||||
"A conexão do terminal foi interrompida. Isso pode acontecer quando o servidor reinicia.",
|
||||
|
||||
"common.closeTab": "Fechar aba",
|
||||
"common.dismiss": "Descartar",
|
||||
"common.requestFailed": "Requisição falhou",
|
||||
"common.moreOptions": "Mais opções",
|
||||
"common.learnMore": "Saiba mais",
|
||||
"common.rename": "Renomear",
|
||||
"common.reset": "Redefinir",
|
||||
"common.archive": "Arquivar",
|
||||
"common.delete": "Excluir",
|
||||
"common.close": "Fechar",
|
||||
"common.edit": "Editar",
|
||||
"common.loadMore": "Carregar mais",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Alternar menu",
|
||||
"sidebar.nav.projectsAndSessions": "Projetos e sessões",
|
||||
"sidebar.settings": "Configurações",
|
||||
"sidebar.help": "Ajuda",
|
||||
"sidebar.workspaces.enable": "Habilitar espaços de trabalho",
|
||||
"sidebar.workspaces.disable": "Desabilitar espaços de trabalho",
|
||||
"sidebar.gettingStarted.title": "Começando",
|
||||
"sidebar.gettingStarted.line1": "OpenCode inclui modelos gratuitos para você começar imediatamente.",
|
||||
"sidebar.gettingStarted.line2": "Conecte qualquer provedor para usar modelos, incluindo Claude, GPT, Gemini etc.",
|
||||
"sidebar.project.recentSessions": "Sessões recentes",
|
||||
"sidebar.project.viewAllSessions": "Ver todas as sessões",
|
||||
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.tab.general": "Geral",
|
||||
"settings.tab.shortcuts": "Atalhos",
|
||||
|
||||
"settings.general.section.appearance": "Aparência",
|
||||
"settings.general.section.notifications": "Notificações do sistema",
|
||||
"settings.general.section.sounds": "Efeitos sonoros",
|
||||
|
||||
"settings.general.row.language.title": "Idioma",
|
||||
"settings.general.row.language.description": "Alterar o idioma de exibição do OpenCode",
|
||||
"settings.general.row.appearance.title": "Aparência",
|
||||
"settings.general.row.appearance.description": "Personalize como o OpenCode aparece no seu dispositivo",
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
||||
"settings.general.row.font.title": "Fonte",
|
||||
"settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código",
|
||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||
"font.option.cascadiaCode": "Cascadia Code",
|
||||
"font.option.firaCode": "Fira Code",
|
||||
"font.option.hack": "Hack",
|
||||
"font.option.inconsolata": "Inconsolata",
|
||||
"font.option.intelOneMono": "Intel One Mono",
|
||||
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||
"font.option.mesloLgs": "Meslo LGS",
|
||||
"font.option.robotoMono": "Roboto Mono",
|
||||
"font.option.sourceCodePro": "Source Code Pro",
|
||||
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||
"sound.option.alert01": "Alerta 01",
|
||||
"sound.option.alert02": "Alerta 02",
|
||||
"sound.option.alert03": "Alerta 03",
|
||||
"sound.option.alert04": "Alerta 04",
|
||||
"sound.option.alert05": "Alerta 05",
|
||||
"sound.option.alert06": "Alerta 06",
|
||||
"sound.option.alert07": "Alerta 07",
|
||||
"sound.option.alert08": "Alerta 08",
|
||||
"sound.option.alert09": "Alerta 09",
|
||||
"sound.option.alert10": "Alerta 10",
|
||||
"sound.option.bipbop01": "Bip-bop 01",
|
||||
"sound.option.bipbop02": "Bip-bop 02",
|
||||
"sound.option.bipbop03": "Bip-bop 03",
|
||||
"sound.option.bipbop04": "Bip-bop 04",
|
||||
"sound.option.bipbop05": "Bip-bop 05",
|
||||
"sound.option.bipbop06": "Bip-bop 06",
|
||||
"sound.option.bipbop07": "Bip-bop 07",
|
||||
"sound.option.bipbop08": "Bip-bop 08",
|
||||
"sound.option.bipbop09": "Bip-bop 09",
|
||||
"sound.option.bipbop10": "Bip-bop 10",
|
||||
"sound.option.staplebops01": "Staplebops 01",
|
||||
"sound.option.staplebops02": "Staplebops 02",
|
||||
"sound.option.staplebops03": "Staplebops 03",
|
||||
"sound.option.staplebops04": "Staplebops 04",
|
||||
"sound.option.staplebops05": "Staplebops 05",
|
||||
"sound.option.staplebops06": "Staplebops 06",
|
||||
"sound.option.staplebops07": "Staplebops 07",
|
||||
"sound.option.nope01": "Não 01",
|
||||
"sound.option.nope02": "Não 02",
|
||||
"sound.option.nope03": "Não 03",
|
||||
"sound.option.nope04": "Não 04",
|
||||
"sound.option.nope05": "Não 05",
|
||||
"sound.option.nope06": "Não 06",
|
||||
"sound.option.nope07": "Não 07",
|
||||
"sound.option.nope08": "Não 08",
|
||||
"sound.option.nope09": "Não 09",
|
||||
"sound.option.nope10": "Não 10",
|
||||
"sound.option.nope11": "Não 11",
|
||||
"sound.option.nope12": "Não 12",
|
||||
"sound.option.yup01": "Sim 01",
|
||||
"sound.option.yup02": "Sim 02",
|
||||
"sound.option.yup03": "Sim 03",
|
||||
"sound.option.yup04": "Sim 04",
|
||||
"sound.option.yup05": "Sim 05",
|
||||
"sound.option.yup06": "Sim 06",
|
||||
|
||||
"settings.general.notifications.agent.title": "Agente",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Mostrar notificação do sistema quando o agente estiver completo ou precisar de atenção",
|
||||
"settings.general.notifications.permissions.title": "Permissões",
|
||||
"settings.general.notifications.permissions.description":
|
||||
"Mostrar notificação do sistema quando uma permissão for necessária",
|
||||
"settings.general.notifications.errors.title": "Erros",
|
||||
"settings.general.notifications.errors.description": "Mostrar notificação do sistema quando ocorrer um erro",
|
||||
|
||||
"settings.general.sounds.agent.title": "Agente",
|
||||
"settings.general.sounds.agent.description": "Reproduzir som quando o agente estiver completo ou precisar de atenção",
|
||||
"settings.general.sounds.permissions.title": "Permissões",
|
||||
"settings.general.sounds.permissions.description": "Reproduzir som quando uma permissão for necessária",
|
||||
"settings.general.sounds.errors.title": "Erros",
|
||||
"settings.general.sounds.errors.description": "Reproduzir som quando ocorrer um erro",
|
||||
|
||||
"settings.shortcuts.title": "Atalhos de teclado",
|
||||
"settings.shortcuts.reset.button": "Redefinir para padrões",
|
||||
"settings.shortcuts.reset.toast.title": "Atalhos redefinidos",
|
||||
"settings.shortcuts.reset.toast.description": "Atalhos de teclado foram redefinidos para os padrões.",
|
||||
"settings.shortcuts.conflict.title": "Atalho já em uso",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} já está atribuído a {{titles}}.",
|
||||
"settings.shortcuts.unassigned": "Não atribuído",
|
||||
"settings.shortcuts.pressKeys": "Pressione teclas",
|
||||
"settings.shortcuts.search.placeholder": "Buscar atalhos",
|
||||
"settings.shortcuts.search.empty": "Nenhum atalho encontrado",
|
||||
|
||||
"settings.shortcuts.group.general": "Geral",
|
||||
"settings.shortcuts.group.session": "Sessão",
|
||||
"settings.shortcuts.group.navigation": "Navegação",
|
||||
"settings.shortcuts.group.modelAndAgent": "Modelo e agente",
|
||||
"settings.shortcuts.group.terminal": "Terminal",
|
||||
"settings.shortcuts.group.prompt": "Prompt",
|
||||
|
||||
"settings.providers.title": "Provedores",
|
||||
"settings.providers.description": "Configurações de provedores estarão disponíveis aqui.",
|
||||
"settings.models.title": "Modelos",
|
||||
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
|
||||
"settings.agents.title": "Agentes",
|
||||
"settings.agents.description": "Configurações de agentes estarão disponíveis aqui.",
|
||||
"settings.commands.title": "Comandos",
|
||||
"settings.commands.description": "Configurações de comandos estarão disponíveis aqui.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "Configurações de MCP estarão disponíveis aqui.",
|
||||
|
||||
"settings.permissions.title": "Permissões",
|
||||
"settings.permissions.description": "Controle quais ferramentas o servidor pode usar por padrão.",
|
||||
"settings.permissions.section.tools": "Ferramentas",
|
||||
"settings.permissions.toast.updateFailed.title": "Falha ao atualizar permissões",
|
||||
|
||||
"settings.permissions.action.allow": "Permitir",
|
||||
"settings.permissions.action.ask": "Perguntar",
|
||||
"settings.permissions.action.deny": "Negar",
|
||||
|
||||
"settings.permissions.tool.read.title": "Ler",
|
||||
"settings.permissions.tool.read.description": "Ler um arquivo (corresponde ao caminho do arquivo)",
|
||||
"settings.permissions.tool.edit.title": "Editar",
|
||||
"settings.permissions.tool.edit.description":
|
||||
"Modificar arquivos, incluindo edições, escritas, patches e multi-edições",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Corresponder arquivos usando padrões glob",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description": "Buscar conteúdo de arquivos usando expressões regulares",
|
||||
"settings.permissions.tool.list.title": "Listar",
|
||||
"settings.permissions.tool.list.description": "Listar arquivos dentro de um diretório",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "Executar comandos shell",
|
||||
"settings.permissions.tool.task.title": "Tarefa",
|
||||
"settings.permissions.tool.task.description": "Lançar sub-agentes",
|
||||
"settings.permissions.tool.skill.title": "Habilidade",
|
||||
"settings.permissions.tool.skill.description": "Carregar uma habilidade por nome",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "Executar consultas de servidor de linguagem",
|
||||
"settings.permissions.tool.todoread.title": "Ler Tarefas",
|
||||
"settings.permissions.tool.todoread.description": "Ler a lista de tarefas",
|
||||
"settings.permissions.tool.todowrite.title": "Escrever Tarefas",
|
||||
"settings.permissions.tool.todowrite.description": "Atualizar a lista de tarefas",
|
||||
"settings.permissions.tool.webfetch.title": "Buscar Web",
|
||||
"settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL",
|
||||
"settings.permissions.tool.websearch.title": "Pesquisa Web",
|
||||
"settings.permissions.tool.websearch.description": "Pesquisar na web",
|
||||
"settings.permissions.tool.codesearch.title": "Pesquisa de Código",
|
||||
"settings.permissions.tool.codesearch.description": "Pesquisar código na web",
|
||||
"settings.permissions.tool.external_directory.title": "Diretório Externo",
|
||||
"settings.permissions.tool.external_directory.description": "Acessar arquivos fora do diretório do projeto",
|
||||
"settings.permissions.tool.doom_loop.title": "Loop Infinito",
|
||||
"settings.permissions.tool.doom_loop.description": "Detectar chamadas de ferramentas repetidas com entrada idêntica",
|
||||
|
||||
"session.delete.failed.title": "Falha ao excluir sessão",
|
||||
"session.delete.title": "Excluir sessão",
|
||||
"session.delete.confirm": 'Excluir sessão "{{name}}"?',
|
||||
"session.delete.button": "Excluir sessão",
|
||||
|
||||
"workspace.new": "Novo espaço de trabalho",
|
||||
"workspace.type.local": "local",
|
||||
"workspace.type.sandbox": "sandbox",
|
||||
"workspace.create.failed.title": "Falha ao criar espaço de trabalho",
|
||||
"workspace.delete.failed.title": "Falha ao excluir espaço de trabalho",
|
||||
"workspace.resetting.title": "Redefinindo espaço de trabalho",
|
||||
"workspace.resetting.description": "Isso pode levar um minuto.",
|
||||
"workspace.reset.failed.title": "Falha ao redefinir espaço de trabalho",
|
||||
"workspace.reset.success.title": "Espaço de trabalho redefinido",
|
||||
"workspace.reset.success.description": "Espaço de trabalho agora corresponde ao branch padrão.",
|
||||
"workspace.status.checking": "Verificando alterações não mescladas...",
|
||||
"workspace.status.error": "Não foi possível verificar o status do git.",
|
||||
"workspace.status.clean": "Nenhuma alteração não mesclada detectada.",
|
||||
"workspace.status.dirty": "Alterações não mescladas detectadas neste espaço de trabalho.",
|
||||
"workspace.delete.title": "Excluir espaço de trabalho",
|
||||
"workspace.delete.confirm": 'Excluir espaço de trabalho "{{name}}"?',
|
||||
"workspace.delete.button": "Excluir espaço de trabalho",
|
||||
"workspace.reset.title": "Redefinir espaço de trabalho",
|
||||
"workspace.reset.confirm": 'Redefinir espaço de trabalho "{{name}}"?',
|
||||
"workspace.reset.button": "Redefinir espaço de trabalho",
|
||||
"workspace.reset.archived.none": "Nenhuma sessão ativa será arquivada.",
|
||||
"workspace.reset.archived.one": "1 sessão será arquivada.",
|
||||
"workspace.reset.archived.many": "{{count}} sessões serão arquivadas.",
|
||||
"workspace.reset.note": "Isso redefinirá o espaço de trabalho para corresponder ao branch padrão.",
|
||||
}
|
||||
@@ -86,6 +86,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Andre",
|
||||
"dialog.provider.tag.recommended": "Anbefalet",
|
||||
"dialog.provider.anthropic.note": "Forbind med Claude Pro/Max eller API-nøgle",
|
||||
"dialog.provider.openai.note": "Forbind med ChatGPT Pro/Plus eller API-nøgle",
|
||||
"dialog.provider.copilot.note": "Forbind med Copilot eller API-nøgle",
|
||||
|
||||
"dialog.model.select.title": "Vælg model",
|
||||
"dialog.model.search.placeholder": "Søg modeller",
|
||||
@@ -136,6 +138,7 @@ export const dict = {
|
||||
"model.tag.latest": "Nyeste",
|
||||
|
||||
"common.search.placeholder": "Søg",
|
||||
"common.goBack": "Gå tilbage",
|
||||
"common.loading": "Indlæser",
|
||||
"common.cancel": "Annuller",
|
||||
"common.submit": "Indsend",
|
||||
@@ -181,7 +184,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "brugerdefineret",
|
||||
"prompt.context.active": "aktiv",
|
||||
"prompt.context.includeActiveFile": "Inkluder aktiv fil",
|
||||
"prompt.context.removeActiveFile": "Fjern aktiv fil fra kontekst",
|
||||
"prompt.context.removeFile": "Fjern fil fra kontekst",
|
||||
"prompt.action.attachFile": "Vedhæft fil",
|
||||
"prompt.attachment.remove": "Fjern vedhæftning",
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stop",
|
||||
|
||||
@@ -225,6 +231,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "Ingen server valgt",
|
||||
"dialog.server.default.set": "Sæt nuværende server som standard",
|
||||
"dialog.server.default.clear": "Ryd",
|
||||
"dialog.server.action.remove": "Fjern server",
|
||||
|
||||
"dialog.project.edit.title": "Rediger projekt",
|
||||
"dialog.project.edit.name": "Navn",
|
||||
@@ -233,6 +240,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "Klik eller træk et billede",
|
||||
"dialog.project.edit.icon.recommended": "Anbefalet: 128x128px",
|
||||
"dialog.project.edit.color": "Farve",
|
||||
"dialog.project.edit.color.select": "Vælg farven {{color}}",
|
||||
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note":
|
||||
@@ -267,6 +275,7 @@ export const dict = {
|
||||
"context.usage.usage": "Forbrug",
|
||||
"context.usage.cost": "Omkostning",
|
||||
"context.usage.clickToView": "Klik for at se kontekst",
|
||||
"context.usage.view": "Se kontekstforbrug",
|
||||
|
||||
"language.en": "Engelsk",
|
||||
"language.zh": "Kinesisk (forenklet)",
|
||||
@@ -279,6 +288,9 @@ export const dict = {
|
||||
"language.da": "Dansk",
|
||||
"language.ru": "Russisk",
|
||||
"language.pl": "Polsk",
|
||||
"language.ar": "Arabisk",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Portugisisk (Brasilien)",
|
||||
|
||||
"toast.language.title": "Sprog",
|
||||
"toast.language.description": "Skiftede til {{language}}",
|
||||
@@ -368,6 +380,7 @@ export const dict = {
|
||||
"session.tab.session": "Session",
|
||||
"session.tab.review": "Gennemgang",
|
||||
"session.tab.context": "Kontekst",
|
||||
"session.panel.reviewAndFiles": "Gennemgang og filer",
|
||||
"session.review.filesChanged": "{{count}} Filer ændret",
|
||||
"session.review.loadingChanges": "Indlæser ændringer...",
|
||||
"session.review.empty": "Ingen ændringer i denne session endnu",
|
||||
@@ -384,6 +397,7 @@ export const dict = {
|
||||
"session.new.lastModified": "Sidst ændret",
|
||||
|
||||
"session.header.search.placeholder": "Søg {{project}}",
|
||||
"session.header.searchFiles": "Søg efter filer",
|
||||
|
||||
"session.share.popover.title": "Udgiv på nettet",
|
||||
"session.share.popover.description.shared":
|
||||
@@ -406,6 +420,7 @@ export const dict = {
|
||||
"terminal.loading": "Indlæser terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Luk terminal",
|
||||
|
||||
"common.closeTab": "Luk fane",
|
||||
"common.dismiss": "Afvis",
|
||||
@@ -414,11 +429,13 @@ export const dict = {
|
||||
"common.learnMore": "Lær mere",
|
||||
"common.rename": "Omdøb",
|
||||
"common.reset": "Nulstil",
|
||||
"common.archive": "Arkivér",
|
||||
"common.delete": "Slet",
|
||||
"common.close": "Luk",
|
||||
"common.edit": "Rediger",
|
||||
"common.loadMore": "Indlæs flere",
|
||||
|
||||
"sidebar.nav.projectsAndSessions": "Projekter og sessioner",
|
||||
"sidebar.settings": "Indstillinger",
|
||||
"sidebar.help": "Hjælp",
|
||||
"sidebar.workspaces.enable": "Aktiver arbejdsområder",
|
||||
@@ -533,6 +550,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "Opdag gentagne værktøjskald med identisk input",
|
||||
|
||||
"session.delete.failed.title": "Kunne ikke slette session",
|
||||
"session.delete.title": "Slet session",
|
||||
"session.delete.confirm": 'Slet session "{{name}}"?',
|
||||
"session.delete.button": "Slet session",
|
||||
|
||||
"workspace.new": "Nyt arbejdsområde",
|
||||
"workspace.type.local": "lokal",
|
||||
"workspace.type.sandbox": "sandkasse",
|
||||
|
||||
@@ -90,6 +90,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Andere",
|
||||
"dialog.provider.tag.recommended": "Empfohlen",
|
||||
"dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden",
|
||||
"dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden",
|
||||
"dialog.provider.copilot.note": "Mit Copilot oder API-Schlüssel verbinden",
|
||||
|
||||
"dialog.model.select.title": "Modell auswählen",
|
||||
"dialog.model.search.placeholder": "Modelle durchsuchen",
|
||||
@@ -140,6 +142,7 @@ export const dict = {
|
||||
"model.tag.latest": "Neueste",
|
||||
|
||||
"common.search.placeholder": "Suchen",
|
||||
"common.goBack": "Zurück",
|
||||
"common.loading": "Laden",
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.submit": "Absenden",
|
||||
@@ -185,7 +188,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "benutzerdefiniert",
|
||||
"prompt.context.active": "aktiv",
|
||||
"prompt.context.includeActiveFile": "Aktive Datei einbeziehen",
|
||||
"prompt.context.removeActiveFile": "Aktive Datei aus dem Kontext entfernen",
|
||||
"prompt.context.removeFile": "Datei aus dem Kontext entfernen",
|
||||
"prompt.action.attachFile": "Datei anhängen",
|
||||
"prompt.attachment.remove": "Anhang entfernen",
|
||||
"prompt.action.send": "Senden",
|
||||
"prompt.action.stop": "Stopp",
|
||||
|
||||
@@ -230,6 +236,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "Kein Server ausgewählt",
|
||||
"dialog.server.default.set": "Aktuellen Server als Standard setzen",
|
||||
"dialog.server.default.clear": "Löschen",
|
||||
"dialog.server.action.remove": "Server entfernen",
|
||||
|
||||
"dialog.project.edit.title": "Projekt bearbeiten",
|
||||
"dialog.project.edit.name": "Name",
|
||||
@@ -238,6 +245,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "Klicken oder Bild ziehen",
|
||||
"dialog.project.edit.icon.recommended": "Empfohlen: 128x128px",
|
||||
"dialog.project.edit.color": "Farbe",
|
||||
"dialog.project.edit.color.select": "{{color}}-Farbe auswählen",
|
||||
|
||||
"context.breakdown.title": "Kontext-Aufschlüsselung",
|
||||
"context.breakdown.note":
|
||||
@@ -272,6 +280,7 @@ export const dict = {
|
||||
"context.usage.usage": "Nutzung",
|
||||
"context.usage.cost": "Kosten",
|
||||
"context.usage.clickToView": "Klicken, um Kontext anzuzeigen",
|
||||
"context.usage.view": "Kontextnutzung anzeigen",
|
||||
|
||||
"language.en": "Englisch",
|
||||
"language.zh": "Chinesisch (Vereinfacht)",
|
||||
@@ -284,6 +293,9 @@ export const dict = {
|
||||
"language.da": "Dänisch",
|
||||
"language.ru": "Russisch",
|
||||
"language.pl": "Polnisch",
|
||||
"language.ar": "Arabisch",
|
||||
"language.no": "Norwegisch",
|
||||
"language.br": "Portugiesisch (Brasilien)",
|
||||
|
||||
"toast.language.title": "Sprache",
|
||||
"toast.language.description": "Zu {{language}} gewechselt",
|
||||
@@ -375,6 +387,7 @@ export const dict = {
|
||||
"session.tab.session": "Sitzung",
|
||||
"session.tab.review": "Überprüfung",
|
||||
"session.tab.context": "Kontext",
|
||||
"session.panel.reviewAndFiles": "Überprüfung und Dateien",
|
||||
"session.review.filesChanged": "{{count}} Dateien geändert",
|
||||
"session.review.loadingChanges": "Lade Änderungen...",
|
||||
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
|
||||
@@ -391,6 +404,7 @@ export const dict = {
|
||||
"session.new.lastModified": "Zuletzt geändert",
|
||||
|
||||
"session.header.search.placeholder": "{{project}} durchsuchen",
|
||||
"session.header.searchFiles": "Dateien suchen",
|
||||
|
||||
"session.share.popover.title": "Im Web veröffentlichen",
|
||||
"session.share.popover.description.shared":
|
||||
@@ -413,6 +427,7 @@ export const dict = {
|
||||
"terminal.loading": "Lade Terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Terminal schließen",
|
||||
|
||||
"common.closeTab": "Tab schließen",
|
||||
"common.dismiss": "Verwerfen",
|
||||
@@ -421,11 +436,13 @@ export const dict = {
|
||||
"common.learnMore": "Mehr erfahren",
|
||||
"common.rename": "Umbenennen",
|
||||
"common.reset": "Zurücksetzen",
|
||||
"common.archive": "Archivieren",
|
||||
"common.delete": "Löschen",
|
||||
"common.close": "Schließen",
|
||||
"common.edit": "Bearbeiten",
|
||||
"common.loadMore": "Mehr laden",
|
||||
|
||||
"sidebar.nav.projectsAndSessions": "Projekte und Sitzungen",
|
||||
"sidebar.settings": "Einstellungen",
|
||||
"sidebar.help": "Hilfe",
|
||||
"sidebar.workspaces.enable": "Arbeitsbereiche aktivieren",
|
||||
@@ -542,6 +559,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "Wiederholte Tool-Aufrufe mit identischer Eingabe erkennen",
|
||||
|
||||
"session.delete.failed.title": "Sitzung konnte nicht gelöscht werden",
|
||||
"session.delete.title": "Sitzung löschen",
|
||||
"session.delete.confirm": 'Sitzung "{{name}}" löschen?',
|
||||
"session.delete.button": "Sitzung löschen",
|
||||
|
||||
"workspace.new": "Neuer Arbeitsbereich",
|
||||
"workspace.type.local": "lokal",
|
||||
"workspace.type.sandbox": "Sandbox",
|
||||
|
||||
@@ -88,6 +88,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Other",
|
||||
"dialog.provider.tag.recommended": "Recommended",
|
||||
"dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key",
|
||||
"dialog.provider.openai.note": "Connect with ChatGPT Pro/Plus or API key",
|
||||
"dialog.provider.copilot.note": "Connect with Copilot or API key",
|
||||
|
||||
"dialog.model.select.title": "Select model",
|
||||
"dialog.model.search.placeholder": "Search models",
|
||||
@@ -153,6 +155,7 @@ export const dict = {
|
||||
"model.tooltip.context": "Context limit {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Search",
|
||||
"common.goBack": "Go back",
|
||||
"common.loading": "Loading",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Cancel",
|
||||
@@ -199,7 +202,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "custom",
|
||||
"prompt.context.active": "active",
|
||||
"prompt.context.includeActiveFile": "Include active file",
|
||||
"prompt.context.removeActiveFile": "Remove active file from context",
|
||||
"prompt.context.removeFile": "Remove file from context",
|
||||
"prompt.action.attachFile": "Attach file",
|
||||
"prompt.attachment.remove": "Remove attachment",
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stop",
|
||||
|
||||
@@ -243,6 +249,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "No server selected",
|
||||
"dialog.server.default.set": "Set current server as default",
|
||||
"dialog.server.default.clear": "Clear",
|
||||
"dialog.server.action.remove": "Remove server",
|
||||
|
||||
"dialog.project.edit.title": "Edit project",
|
||||
"dialog.project.edit.name": "Name",
|
||||
@@ -251,6 +258,10 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "Click or drag an image",
|
||||
"dialog.project.edit.icon.recommended": "Recommended: 128x128px",
|
||||
"dialog.project.edit.color": "Color",
|
||||
"dialog.project.edit.color.select": "Select {{color}} color",
|
||||
"dialog.project.edit.worktree.startup": "Workspace startup script",
|
||||
"dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "e.g. bun install",
|
||||
|
||||
"context.breakdown.title": "Context Breakdown",
|
||||
"context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',
|
||||
@@ -284,6 +295,7 @@ export const dict = {
|
||||
"context.usage.usage": "Usage",
|
||||
"context.usage.cost": "Cost",
|
||||
"context.usage.clickToView": "Click to view context",
|
||||
"context.usage.view": "View context usage",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "Chinese (Simplified)",
|
||||
@@ -296,6 +308,9 @@ export const dict = {
|
||||
"language.da": "Danish",
|
||||
"language.ru": "Russian",
|
||||
"language.pl": "Polish",
|
||||
"language.ar": "Arabic",
|
||||
"language.no": "Norwegian",
|
||||
"language.br": "Portuguese (Brazil)",
|
||||
|
||||
"toast.language.title": "Language",
|
||||
"toast.language.description": "Switched to {{language}}",
|
||||
@@ -385,6 +400,7 @@ export const dict = {
|
||||
"session.tab.session": "Session",
|
||||
"session.tab.review": "Review",
|
||||
"session.tab.context": "Context",
|
||||
"session.panel.reviewAndFiles": "Review and files",
|
||||
"session.review.filesChanged": "{{count}} Files Changed",
|
||||
"session.review.loadingChanges": "Loading changes...",
|
||||
"session.review.empty": "No changes in this session yet",
|
||||
@@ -402,6 +418,7 @@ export const dict = {
|
||||
"session.new.lastModified": "Last modified",
|
||||
|
||||
"session.header.search.placeholder": "Search {{project}}",
|
||||
"session.header.searchFiles": "Search files",
|
||||
|
||||
"session.share.popover.title": "Publish on web",
|
||||
"session.share.popover.description.shared":
|
||||
@@ -424,6 +441,7 @@ export const dict = {
|
||||
"terminal.loading": "Loading terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Close terminal",
|
||||
"terminal.connectionLost.title": "Connection Lost",
|
||||
"terminal.connectionLost.description":
|
||||
"The terminal connection was interrupted. This can happen when the server restarts.",
|
||||
@@ -435,6 +453,7 @@ export const dict = {
|
||||
"common.learnMore": "Learn more",
|
||||
"common.rename": "Rename",
|
||||
"common.reset": "Reset",
|
||||
"common.archive": "Archive",
|
||||
"common.delete": "Delete",
|
||||
"common.close": "Close",
|
||||
"common.edit": "Edit",
|
||||
@@ -442,6 +461,7 @@ export const dict = {
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Toggle menu",
|
||||
"sidebar.nav.projectsAndSessions": "Projects and sessions",
|
||||
"sidebar.settings": "Settings",
|
||||
"sidebar.help": "Help",
|
||||
"sidebar.workspaces.enable": "Enable workspaces",
|
||||
@@ -611,6 +631,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "Detect repeated tool calls with identical input",
|
||||
|
||||
"session.delete.failed.title": "Failed to delete session",
|
||||
"session.delete.title": "Delete session",
|
||||
"session.delete.confirm": 'Delete session "{{name}}"?',
|
||||
"session.delete.button": "Delete session",
|
||||
|
||||
"workspace.new": "New workspace",
|
||||
"workspace.type.local": "local",
|
||||
"workspace.type.sandbox": "sandbox",
|
||||
|
||||
@@ -86,6 +86,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Otro",
|
||||
"dialog.provider.tag.recommended": "Recomendado",
|
||||
"dialog.provider.anthropic.note": "Conectar con Claude Pro/Max o clave API",
|
||||
"dialog.provider.openai.note": "Conectar con ChatGPT Pro/Plus o clave API",
|
||||
"dialog.provider.copilot.note": "Conectar con Copilot o clave API",
|
||||
|
||||
"dialog.model.select.title": "Seleccionar modelo",
|
||||
"dialog.model.search.placeholder": "Buscar modelos",
|
||||
@@ -136,6 +138,7 @@ export const dict = {
|
||||
"model.tag.latest": "Último",
|
||||
|
||||
"common.search.placeholder": "Buscar",
|
||||
"common.goBack": "Volver",
|
||||
"common.loading": "Cargando",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.submit": "Enviar",
|
||||
@@ -181,7 +184,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "personalizado",
|
||||
"prompt.context.active": "activo",
|
||||
"prompt.context.includeActiveFile": "Incluir archivo activo",
|
||||
"prompt.context.removeActiveFile": "Eliminar archivo activo del contexto",
|
||||
"prompt.context.removeFile": "Eliminar archivo del contexto",
|
||||
"prompt.action.attachFile": "Adjuntar archivo",
|
||||
"prompt.attachment.remove": "Eliminar adjunto",
|
||||
"prompt.action.send": "Enviar",
|
||||
"prompt.action.stop": "Detener",
|
||||
|
||||
@@ -225,6 +231,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "Ningún servidor seleccionado",
|
||||
"dialog.server.default.set": "Establecer servidor actual como predeterminado",
|
||||
"dialog.server.default.clear": "Limpiar",
|
||||
"dialog.server.action.remove": "Eliminar servidor",
|
||||
|
||||
"dialog.project.edit.title": "Editar proyecto",
|
||||
"dialog.project.edit.name": "Nombre",
|
||||
@@ -233,6 +240,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "Haz clic o arrastra una imagen",
|
||||
"dialog.project.edit.icon.recommended": "Recomendado: 128x128px",
|
||||
"dialog.project.edit.color": "Color",
|
||||
"dialog.project.edit.color.select": "Seleccionar color {{color}}",
|
||||
|
||||
"context.breakdown.title": "Desglose de Contexto",
|
||||
"context.breakdown.note":
|
||||
@@ -267,6 +275,7 @@ export const dict = {
|
||||
"context.usage.usage": "Uso",
|
||||
"context.usage.cost": "Costo",
|
||||
"context.usage.clickToView": "Haz clic para ver contexto",
|
||||
"context.usage.view": "Ver uso del contexto",
|
||||
|
||||
"language.en": "Inglés",
|
||||
"language.zh": "Chino (simplificado)",
|
||||
@@ -279,6 +288,9 @@ export const dict = {
|
||||
"language.da": "Danés",
|
||||
"language.ru": "Ruso",
|
||||
"language.pl": "Polaco",
|
||||
"language.ar": "Árabe",
|
||||
"language.no": "Noruego",
|
||||
"language.br": "Portugués (Brasil)",
|
||||
|
||||
"toast.language.title": "Idioma",
|
||||
"toast.language.description": "Cambiado a {{language}}",
|
||||
@@ -369,6 +381,7 @@ export const dict = {
|
||||
"session.tab.session": "Sesión",
|
||||
"session.tab.review": "Revisión",
|
||||
"session.tab.context": "Contexto",
|
||||
"session.panel.reviewAndFiles": "Revisión y archivos",
|
||||
"session.review.filesChanged": "{{count}} Archivos Cambiados",
|
||||
"session.review.loadingChanges": "Cargando cambios...",
|
||||
"session.review.empty": "No hay cambios en esta sesión aún",
|
||||
@@ -385,6 +398,7 @@ export const dict = {
|
||||
"session.new.lastModified": "Última modificación",
|
||||
|
||||
"session.header.search.placeholder": "Buscar {{project}}",
|
||||
"session.header.searchFiles": "Buscar archivos",
|
||||
|
||||
"session.share.popover.title": "Publicar en web",
|
||||
"session.share.popover.description.shared":
|
||||
@@ -407,6 +421,7 @@ export const dict = {
|
||||
"terminal.loading": "Cargando terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Cerrar terminal",
|
||||
|
||||
"common.closeTab": "Cerrar pestaña",
|
||||
"common.dismiss": "Descartar",
|
||||
@@ -415,11 +430,13 @@ export const dict = {
|
||||
"common.learnMore": "Saber más",
|
||||
"common.rename": "Renombrar",
|
||||
"common.reset": "Restablecer",
|
||||
"common.archive": "Archivar",
|
||||
"common.delete": "Eliminar",
|
||||
"common.close": "Cerrar",
|
||||
"common.edit": "Editar",
|
||||
"common.loadMore": "Cargar más",
|
||||
|
||||
"sidebar.nav.projectsAndSessions": "Proyectos y sesiones",
|
||||
"sidebar.settings": "Ajustes",
|
||||
"sidebar.help": "Ayuda",
|
||||
"sidebar.workspaces.enable": "Habilitar espacios de trabajo",
|
||||
@@ -536,6 +553,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Bucle Infinito",
|
||||
"settings.permissions.tool.doom_loop.description": "Detectar llamadas a herramientas repetidas con entrada idéntica",
|
||||
|
||||
"session.delete.failed.title": "Fallo al eliminar sesión",
|
||||
"session.delete.title": "Eliminar sesión",
|
||||
"session.delete.confirm": '¿Eliminar sesión "{{name}}"?',
|
||||
"session.delete.button": "Eliminar sesión",
|
||||
|
||||
"workspace.new": "Nuevo espacio de trabajo",
|
||||
"workspace.type.local": "local",
|
||||
"workspace.type.sandbox": "sandbox",
|
||||
|
||||
@@ -86,6 +86,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Autre",
|
||||
"dialog.provider.tag.recommended": "Recommandé",
|
||||
"dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API",
|
||||
"dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API",
|
||||
"dialog.provider.copilot.note": "Connectez-vous avec Copilot ou une clé API",
|
||||
|
||||
"dialog.model.select.title": "Sélectionner un modèle",
|
||||
"dialog.model.search.placeholder": "Rechercher des modèles",
|
||||
@@ -136,6 +138,7 @@ export const dict = {
|
||||
"model.tag.latest": "Dernier",
|
||||
|
||||
"common.search.placeholder": "Rechercher",
|
||||
"common.goBack": "Retour",
|
||||
"common.loading": "Chargement",
|
||||
"common.cancel": "Annuler",
|
||||
"common.submit": "Soumettre",
|
||||
@@ -181,7 +184,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "personnalisé",
|
||||
"prompt.context.active": "actif",
|
||||
"prompt.context.includeActiveFile": "Inclure le fichier actif",
|
||||
"prompt.context.removeActiveFile": "Retirer le fichier actif du contexte",
|
||||
"prompt.context.removeFile": "Retirer le fichier du contexte",
|
||||
"prompt.action.attachFile": "Joindre un fichier",
|
||||
"prompt.attachment.remove": "Supprimer la pièce jointe",
|
||||
"prompt.action.send": "Envoyer",
|
||||
"prompt.action.stop": "Arrêter",
|
||||
|
||||
@@ -225,6 +231,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "Aucun serveur sélectionné",
|
||||
"dialog.server.default.set": "Définir le serveur actuel comme défaut",
|
||||
"dialog.server.default.clear": "Effacer",
|
||||
"dialog.server.action.remove": "Supprimer le serveur",
|
||||
|
||||
"dialog.project.edit.title": "Modifier le projet",
|
||||
"dialog.project.edit.name": "Nom",
|
||||
@@ -233,6 +240,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "Cliquez ou faites glisser une image",
|
||||
"dialog.project.edit.icon.recommended": "Recommandé : 128x128px",
|
||||
"dialog.project.edit.color": "Couleur",
|
||||
"dialog.project.edit.color.select": "Sélectionner la couleur {{color}}",
|
||||
|
||||
"context.breakdown.title": "Répartition du contexte",
|
||||
"context.breakdown.note":
|
||||
@@ -267,6 +275,7 @@ export const dict = {
|
||||
"context.usage.usage": "Utilisation",
|
||||
"context.usage.cost": "Coût",
|
||||
"context.usage.clickToView": "Cliquez pour voir le contexte",
|
||||
"context.usage.view": "Voir l'utilisation du contexte",
|
||||
|
||||
"language.en": "Anglais",
|
||||
"language.zh": "Chinois (simplifié)",
|
||||
@@ -279,6 +288,9 @@ export const dict = {
|
||||
"language.da": "Danois",
|
||||
"language.ru": "Russe",
|
||||
"language.pl": "Polonais",
|
||||
"language.ar": "Arabe",
|
||||
"language.no": "Norvégien",
|
||||
"language.br": "Portugais (Brésil)",
|
||||
|
||||
"toast.language.title": "Langue",
|
||||
"toast.language.description": "Passé à {{language}}",
|
||||
@@ -374,6 +386,7 @@ export const dict = {
|
||||
"session.tab.session": "Session",
|
||||
"session.tab.review": "Revue",
|
||||
"session.tab.context": "Contexte",
|
||||
"session.panel.reviewAndFiles": "Revue et fichiers",
|
||||
"session.review.filesChanged": "{{count}} fichiers modifiés",
|
||||
"session.review.loadingChanges": "Chargement des modifications...",
|
||||
"session.review.empty": "Aucune modification dans cette session pour l'instant",
|
||||
@@ -390,6 +403,7 @@ export const dict = {
|
||||
"session.new.lastModified": "Dernière modification",
|
||||
|
||||
"session.header.search.placeholder": "Rechercher {{project}}",
|
||||
"session.header.searchFiles": "Rechercher des fichiers",
|
||||
|
||||
"session.share.popover.title": "Publier sur le web",
|
||||
"session.share.popover.description.shared":
|
||||
@@ -412,6 +426,7 @@ export const dict = {
|
||||
"terminal.loading": "Chargement du terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Fermer le terminal",
|
||||
|
||||
"common.closeTab": "Fermer l'onglet",
|
||||
"common.dismiss": "Ignorer",
|
||||
@@ -420,11 +435,13 @@ export const dict = {
|
||||
"common.learnMore": "En savoir plus",
|
||||
"common.rename": "Renommer",
|
||||
"common.reset": "Réinitialiser",
|
||||
"common.archive": "Archiver",
|
||||
"common.delete": "Supprimer",
|
||||
"common.close": "Fermer",
|
||||
"common.edit": "Modifier",
|
||||
"common.loadMore": "Charger plus",
|
||||
|
||||
"sidebar.nav.projectsAndSessions": "Projets et sessions",
|
||||
"sidebar.settings": "Paramètres",
|
||||
"sidebar.help": "Aide",
|
||||
"sidebar.workspaces.enable": "Activer les espaces de travail",
|
||||
@@ -543,6 +560,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Boucle infernale",
|
||||
"settings.permissions.tool.doom_loop.description": "Détecter les appels d'outils répétés avec une entrée identique",
|
||||
|
||||
"session.delete.failed.title": "Échec de la suppression de la session",
|
||||
"session.delete.title": "Supprimer la session",
|
||||
"session.delete.confirm": 'Supprimer la session "{{name}}" ?',
|
||||
"session.delete.button": "Supprimer la session",
|
||||
|
||||
"workspace.new": "Nouvel espace de travail",
|
||||
"workspace.type.local": "local",
|
||||
"workspace.type.sandbox": "bac à sable",
|
||||
|
||||
@@ -86,6 +86,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "その他",
|
||||
"dialog.provider.tag.recommended": "推奨",
|
||||
"dialog.provider.anthropic.note": "Claude Pro/MaxまたはAPIキーで接続",
|
||||
"dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続",
|
||||
"dialog.provider.copilot.note": "CopilotまたはAPIキーで接続",
|
||||
|
||||
"dialog.model.select.title": "モデルを選択",
|
||||
"dialog.model.search.placeholder": "モデルを検索",
|
||||
@@ -135,6 +137,7 @@ export const dict = {
|
||||
"model.tag.latest": "最新",
|
||||
|
||||
"common.search.placeholder": "検索",
|
||||
"common.goBack": "戻る",
|
||||
"common.loading": "読み込み中",
|
||||
"common.cancel": "キャンセル",
|
||||
"common.submit": "送信",
|
||||
@@ -180,7 +183,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "カスタム",
|
||||
"prompt.context.active": "アクティブ",
|
||||
"prompt.context.includeActiveFile": "アクティブなファイルを含める",
|
||||
"prompt.context.removeActiveFile": "コンテキストからアクティブなファイルを削除",
|
||||
"prompt.context.removeFile": "コンテキストからファイルを削除",
|
||||
"prompt.action.attachFile": "ファイルを添付",
|
||||
"prompt.attachment.remove": "添付ファイルを削除",
|
||||
"prompt.action.send": "送信",
|
||||
"prompt.action.stop": "停止",
|
||||
|
||||
@@ -224,6 +230,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "サーバーが選択されていません",
|
||||
"dialog.server.default.set": "現在のサーバーをデフォルトに設定",
|
||||
"dialog.server.default.clear": "クリア",
|
||||
"dialog.server.action.remove": "サーバーを削除",
|
||||
|
||||
"dialog.project.edit.title": "プロジェクトを編集",
|
||||
"dialog.project.edit.name": "名前",
|
||||
@@ -232,6 +239,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "クリックまたは画像をドラッグ",
|
||||
"dialog.project.edit.icon.recommended": "推奨: 128x128px",
|
||||
"dialog.project.edit.color": "色",
|
||||
"dialog.project.edit.color.select": "{{color}}の色を選択",
|
||||
|
||||
"context.breakdown.title": "コンテキストの内訳",
|
||||
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
|
||||
@@ -265,6 +273,7 @@ export const dict = {
|
||||
"context.usage.usage": "使用量",
|
||||
"context.usage.cost": "コスト",
|
||||
"context.usage.clickToView": "クリックしてコンテキストを表示",
|
||||
"context.usage.view": "コンテキスト使用量を表示",
|
||||
|
||||
"language.en": "英語",
|
||||
"language.zh": "中国語(簡体字)",
|
||||
@@ -277,6 +286,9 @@ export const dict = {
|
||||
"language.da": "デンマーク語",
|
||||
"language.ru": "ロシア語",
|
||||
"language.pl": "ポーランド語",
|
||||
"language.ar": "アラビア語",
|
||||
"language.no": "ノルウェー語",
|
||||
"language.br": "ポルトガル語(ブラジル)",
|
||||
|
||||
"toast.language.title": "言語",
|
||||
"toast.language.description": "{{language}}に切り替えました",
|
||||
@@ -366,6 +378,7 @@ export const dict = {
|
||||
"session.tab.session": "セッション",
|
||||
"session.tab.review": "レビュー",
|
||||
"session.tab.context": "コンテキスト",
|
||||
"session.panel.reviewAndFiles": "レビューとファイル",
|
||||
"session.review.filesChanged": "{{count}} ファイル変更",
|
||||
"session.review.loadingChanges": "変更を読み込み中...",
|
||||
"session.review.empty": "このセッションでの変更はまだありません",
|
||||
@@ -382,6 +395,7 @@ export const dict = {
|
||||
"session.new.lastModified": "最終更新",
|
||||
|
||||
"session.header.search.placeholder": "{{project}}を検索",
|
||||
"session.header.searchFiles": "ファイルを検索",
|
||||
|
||||
"session.share.popover.title": "ウェブで公開",
|
||||
"session.share.popover.description.shared":
|
||||
@@ -404,6 +418,7 @@ export const dict = {
|
||||
"terminal.loading": "ターミナルを読み込み中...",
|
||||
"terminal.title": "ターミナル",
|
||||
"terminal.title.numbered": "ターミナル {{number}}",
|
||||
"terminal.close": "ターミナルを閉じる",
|
||||
|
||||
"common.closeTab": "タブを閉じる",
|
||||
"common.dismiss": "閉じる",
|
||||
@@ -412,11 +427,13 @@ export const dict = {
|
||||
"common.learnMore": "詳細",
|
||||
"common.rename": "名前変更",
|
||||
"common.reset": "リセット",
|
||||
"common.archive": "アーカイブ",
|
||||
"common.delete": "削除",
|
||||
"common.close": "閉じる",
|
||||
"common.edit": "編集",
|
||||
"common.loadMore": "さらに読み込む",
|
||||
|
||||
"sidebar.nav.projectsAndSessions": "プロジェクトとセッション",
|
||||
"sidebar.settings": "設定",
|
||||
"sidebar.help": "ヘルプ",
|
||||
"sidebar.workspaces.enable": "ワークスペースを有効化",
|
||||
@@ -530,6 +547,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "同一入力による繰り返しのツール呼び出しを検出",
|
||||
|
||||
"session.delete.failed.title": "セッションの削除に失敗しました",
|
||||
"session.delete.title": "セッションの削除",
|
||||
"session.delete.confirm": 'セッション "{{name}}" を削除しますか?',
|
||||
"session.delete.button": "セッションを削除",
|
||||
|
||||
"workspace.new": "新しいワークスペース",
|
||||
"workspace.type.local": "ローカル",
|
||||
"workspace.type.sandbox": "サンドボックス",
|
||||
|
||||
@@ -90,6 +90,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "기타",
|
||||
"dialog.provider.tag.recommended": "추천",
|
||||
"dialog.provider.anthropic.note": "Claude Pro/Max 또는 API 키로 연결",
|
||||
"dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결",
|
||||
"dialog.provider.copilot.note": "Copilot 또는 API 키로 연결",
|
||||
|
||||
"dialog.model.select.title": "모델 선택",
|
||||
"dialog.model.search.placeholder": "모델 검색",
|
||||
@@ -139,6 +141,7 @@ export const dict = {
|
||||
"model.tag.latest": "최신",
|
||||
|
||||
"common.search.placeholder": "검색",
|
||||
"common.goBack": "뒤로 가기",
|
||||
"common.loading": "로딩 중",
|
||||
"common.cancel": "취소",
|
||||
"common.submit": "제출",
|
||||
@@ -184,7 +187,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "사용자 지정",
|
||||
"prompt.context.active": "활성",
|
||||
"prompt.context.includeActiveFile": "활성 파일 포함",
|
||||
"prompt.context.removeActiveFile": "컨텍스트에서 활성 파일 제거",
|
||||
"prompt.context.removeFile": "컨텍스트에서 파일 제거",
|
||||
"prompt.action.attachFile": "파일 첨부",
|
||||
"prompt.attachment.remove": "첨부 파일 제거",
|
||||
"prompt.action.send": "전송",
|
||||
"prompt.action.stop": "중지",
|
||||
|
||||
@@ -228,6 +234,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "선택된 서버 없음",
|
||||
"dialog.server.default.set": "현재 서버를 기본값으로 설정",
|
||||
"dialog.server.default.clear": "지우기",
|
||||
"dialog.server.action.remove": "서버 제거",
|
||||
|
||||
"dialog.project.edit.title": "프로젝트 편집",
|
||||
"dialog.project.edit.name": "이름",
|
||||
@@ -236,6 +243,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "이미지를 클릭하거나 드래그하세요",
|
||||
"dialog.project.edit.icon.recommended": "권장: 128x128px",
|
||||
"dialog.project.edit.color": "색상",
|
||||
"dialog.project.edit.color.select": "{{color}} 색상 선택",
|
||||
|
||||
"context.breakdown.title": "컨텍스트 분석",
|
||||
"context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.',
|
||||
@@ -269,6 +277,7 @@ export const dict = {
|
||||
"context.usage.usage": "사용량",
|
||||
"context.usage.cost": "비용",
|
||||
"context.usage.clickToView": "컨텍스트를 보려면 클릭",
|
||||
"context.usage.view": "컨텍스트 사용량 보기",
|
||||
|
||||
"language.en": "영어",
|
||||
"language.zh": "중국어 (간체)",
|
||||
@@ -281,6 +290,9 @@ export const dict = {
|
||||
"language.da": "덴마크어",
|
||||
"language.ru": "러시아어",
|
||||
"language.pl": "폴란드어",
|
||||
"language.ar": "아랍어",
|
||||
"language.no": "노르웨이어",
|
||||
"language.br": "포르투갈어 (브라질)",
|
||||
|
||||
"toast.language.title": "언어",
|
||||
"toast.language.description": "{{language}}(으)로 전환됨",
|
||||
@@ -369,6 +381,7 @@ export const dict = {
|
||||
"session.tab.session": "세션",
|
||||
"session.tab.review": "검토",
|
||||
"session.tab.context": "컨텍스트",
|
||||
"session.panel.reviewAndFiles": "검토 및 파일",
|
||||
"session.review.filesChanged": "{{count}}개 파일 변경됨",
|
||||
"session.review.loadingChanges": "변경 사항 로드 중...",
|
||||
"session.review.empty": "이 세션에 변경 사항이 아직 없습니다",
|
||||
@@ -385,6 +398,7 @@ export const dict = {
|
||||
"session.new.lastModified": "최근 수정",
|
||||
|
||||
"session.header.search.placeholder": "{{project}} 검색",
|
||||
"session.header.searchFiles": "파일 검색",
|
||||
|
||||
"session.share.popover.title": "웹에 게시",
|
||||
"session.share.popover.description.shared": "이 세션은 웹에 공개되었습니다. 링크가 있는 누구나 액세스할 수 있습니다.",
|
||||
@@ -406,6 +420,7 @@ export const dict = {
|
||||
"terminal.loading": "터미널 로드 중...",
|
||||
"terminal.title": "터미널",
|
||||
"terminal.title.numbered": "터미널 {{number}}",
|
||||
"terminal.close": "터미널 닫기",
|
||||
|
||||
"common.closeTab": "탭 닫기",
|
||||
"common.dismiss": "닫기",
|
||||
@@ -414,11 +429,13 @@ export const dict = {
|
||||
"common.learnMore": "더 알아보기",
|
||||
"common.rename": "이름 바꾸기",
|
||||
"common.reset": "초기화",
|
||||
"common.archive": "보관",
|
||||
"common.delete": "삭제",
|
||||
"common.close": "닫기",
|
||||
"common.edit": "편집",
|
||||
"common.loadMore": "더 불러오기",
|
||||
|
||||
"sidebar.nav.projectsAndSessions": "프로젝트 및 세션",
|
||||
"sidebar.settings": "설정",
|
||||
"sidebar.help": "도움말",
|
||||
"sidebar.workspaces.enable": "작업 공간 활성화",
|
||||
@@ -531,6 +548,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "무한 반복",
|
||||
"settings.permissions.tool.doom_loop.description": "동일한 입력으로 반복되는 도구 호출 감지",
|
||||
|
||||
"session.delete.failed.title": "세션 삭제 실패",
|
||||
"session.delete.title": "세션 삭제",
|
||||
"session.delete.confirm": '"{{name}}" 세션을 삭제하시겠습니까?',
|
||||
"session.delete.button": "세션 삭제",
|
||||
|
||||
"workspace.new": "새 작업 공간",
|
||||
"workspace.type.local": "로컬",
|
||||
"workspace.type.sandbox": "샌드박스",
|
||||
|
||||
602
packages/app/src/i18n/no.ts
Normal file
602
packages/app/src/i18n/no.ts
Normal file
@@ -0,0 +1,602 @@
|
||||
import { dict as en } from "./en"
|
||||
type Keys = keyof typeof en
|
||||
|
||||
export const dict = {
|
||||
"command.category.suggested": "Foreslått",
|
||||
"command.category.view": "Visning",
|
||||
"command.category.project": "Prosjekt",
|
||||
"command.category.provider": "Leverandør",
|
||||
"command.category.server": "Server",
|
||||
"command.category.session": "Sesjon",
|
||||
"command.category.theme": "Tema",
|
||||
"command.category.language": "Språk",
|
||||
"command.category.file": "Fil",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modell",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "Agent",
|
||||
"command.category.permissions": "Tillatelser",
|
||||
"command.category.workspace": "Arbeidsområde",
|
||||
"command.category.settings": "Innstillinger",
|
||||
|
||||
"theme.scheme.system": "System",
|
||||
"theme.scheme.light": "Lys",
|
||||
"theme.scheme.dark": "Mørk",
|
||||
|
||||
"command.sidebar.toggle": "Veksle sidepanel",
|
||||
"command.project.open": "Åpne prosjekt",
|
||||
"command.provider.connect": "Koble til leverandør",
|
||||
"command.server.switch": "Bytt server",
|
||||
"command.settings.open": "Åpne innstillinger",
|
||||
"command.session.previous": "Forrige sesjon",
|
||||
"command.session.next": "Neste sesjon",
|
||||
"command.session.archive": "Arkiver sesjon",
|
||||
|
||||
"command.palette": "Kommandopalett",
|
||||
|
||||
"command.theme.cycle": "Bytt tema",
|
||||
"command.theme.set": "Bruk tema: {{theme}}",
|
||||
"command.theme.scheme.cycle": "Bytt fargevalg",
|
||||
"command.theme.scheme.set": "Bruk fargevalg: {{scheme}}",
|
||||
|
||||
"command.language.cycle": "Bytt språk",
|
||||
"command.language.set": "Bruk språk: {{language}}",
|
||||
|
||||
"command.session.new": "Ny sesjon",
|
||||
"command.file.open": "Åpne fil",
|
||||
"command.file.open.description": "Søk i filer og kommandoer",
|
||||
"command.terminal.toggle": "Veksle terminal",
|
||||
"command.review.toggle": "Veksle gjennomgang",
|
||||
"command.terminal.new": "Ny terminal",
|
||||
"command.terminal.new.description": "Opprett en ny terminalfane",
|
||||
"command.steps.toggle": "Veksle trinn",
|
||||
"command.steps.toggle.description": "Vis eller skjul trinn for gjeldende melding",
|
||||
"command.message.previous": "Forrige melding",
|
||||
"command.message.previous.description": "Gå til forrige brukermelding",
|
||||
"command.message.next": "Neste melding",
|
||||
"command.message.next.description": "Gå til neste brukermelding",
|
||||
"command.model.choose": "Velg modell",
|
||||
"command.model.choose.description": "Velg en annen modell",
|
||||
"command.mcp.toggle": "Veksle MCP-er",
|
||||
"command.mcp.toggle.description": "Veksle MCP-er",
|
||||
"command.agent.cycle": "Bytt agent",
|
||||
"command.agent.cycle.description": "Bytt til neste agent",
|
||||
"command.agent.cycle.reverse": "Bytt agent bakover",
|
||||
"command.agent.cycle.reverse.description": "Bytt til forrige agent",
|
||||
"command.model.variant.cycle": "Bytt tenkeinnsats",
|
||||
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
||||
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
||||
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
||||
"command.session.undo": "Angre",
|
||||
"command.session.undo.description": "Angre siste melding",
|
||||
"command.session.redo": "Gjør om",
|
||||
"command.session.redo.description": "Gjør om siste angrede melding",
|
||||
"command.session.compact": "Komprimer sesjon",
|
||||
"command.session.compact.description": "Oppsummer sesjonen for å redusere kontekststørrelsen",
|
||||
"command.session.fork": "Forgren fra melding",
|
||||
"command.session.fork.description": "Opprett en ny sesjon fra en tidligere melding",
|
||||
"command.session.share": "Del sesjon",
|
||||
"command.session.share.description": "Del denne sesjonen og kopier URL-en til utklippstavlen",
|
||||
"command.session.unshare": "Slutt å dele sesjon",
|
||||
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
||||
|
||||
"palette.search.placeholder": "Søk i filer og kommandoer",
|
||||
"palette.empty": "Ingen resultater funnet",
|
||||
"palette.group.commands": "Kommandoer",
|
||||
"palette.group.files": "Filer",
|
||||
|
||||
"dialog.provider.search.placeholder": "Søk etter leverandører",
|
||||
"dialog.provider.empty": "Ingen leverandører funnet",
|
||||
"dialog.provider.group.popular": "Populære",
|
||||
"dialog.provider.group.other": "Andre",
|
||||
"dialog.provider.tag.recommended": "Anbefalt",
|
||||
"dialog.provider.anthropic.note": "Koble til med Claude Pro/Max eller API-nøkkel",
|
||||
"dialog.provider.openai.note": "Koble til med ChatGPT Pro/Plus eller API-nøkkel",
|
||||
"dialog.provider.copilot.note": "Koble til med Copilot eller API-nøkkel",
|
||||
|
||||
"dialog.model.select.title": "Velg modell",
|
||||
"dialog.model.search.placeholder": "Søk etter modeller",
|
||||
"dialog.model.empty": "Ingen modellresultater",
|
||||
"dialog.model.manage": "Administrer modeller",
|
||||
"dialog.model.manage.description": "Tilpass hvilke modeller som vises i modellvelgeren.",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører",
|
||||
|
||||
"dialog.provider.viewAll": "Vis alle leverandører",
|
||||
|
||||
"provider.connect.title": "Koble til {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Logg inn med Claude Pro/Max",
|
||||
"provider.connect.selectMethod": "Velg innloggingsmetode for {{provider}}.",
|
||||
"provider.connect.method.apiKey": "API-nøkkel",
|
||||
"provider.connect.status.inProgress": "Autorisering pågår...",
|
||||
"provider.connect.status.waiting": "Venter på autorisering...",
|
||||
"provider.connect.status.failed": "Autorisering mislyktes: {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"Skriv inn din {{provider}} API-nøkkel for å koble til kontoen din og bruke {{provider}}-modeller i OpenCode.",
|
||||
"provider.connect.apiKey.label": "{{provider}} API-nøkkel",
|
||||
"provider.connect.apiKey.placeholder": "API-nøkkel",
|
||||
"provider.connect.apiKey.required": "API-nøkkel er påkrevd",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"OpenCode Zen gir deg tilgang til et utvalg av pålitelige optimaliserte modeller for kodeagenter.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Med én enkelt API-nøkkel får du tilgang til modeller som Claude, GPT, Gemini, GLM og flere.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Besøk ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " for å hente API-nøkkelen din.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Besøk ",
|
||||
"provider.connect.oauth.code.visit.link": "denne lenken",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
" for å hente autorisasjonskoden din for å koble til kontoen din og bruke {{provider}}-modeller i OpenCode.",
|
||||
"provider.connect.oauth.code.label": "{{method}} autorisasjonskode",
|
||||
"provider.connect.oauth.code.placeholder": "Autorisasjonskode",
|
||||
"provider.connect.oauth.code.required": "Autorisasjonskode er påkrevd",
|
||||
"provider.connect.oauth.code.invalid": "Ugyldig autorisasjonskode",
|
||||
"provider.connect.oauth.auto.visit.prefix": "Besøk ",
|
||||
"provider.connect.oauth.auto.visit.link": "denne lenken",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" og skriv inn koden nedenfor for å koble til kontoen din og bruke {{provider}}-modeller i OpenCode.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "Bekreftelseskode",
|
||||
"provider.connect.toast.connected.title": "{{provider}} tilkoblet",
|
||||
"provider.connect.toast.connected.description": "{{provider}}-modeller er nå tilgjengelige.",
|
||||
|
||||
"model.tag.free": "Gratis",
|
||||
"model.tag.latest": "Nyeste",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
"model.provider.openai": "OpenAI",
|
||||
"model.provider.google": "Google",
|
||||
"model.provider.xai": "xAI",
|
||||
"model.provider.meta": "Meta",
|
||||
"model.input.text": "tekst",
|
||||
"model.input.image": "bilde",
|
||||
"model.input.audio": "lyd",
|
||||
"model.input.video": "video",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Tillater: {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "Tillater resonnering",
|
||||
"model.tooltip.reasoning.none": "Ingen resonnering",
|
||||
"model.tooltip.context": "Kontekstgrense {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Søk",
|
||||
"common.goBack": "Gå tilbake",
|
||||
"common.loading": "Laster",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Avbryt",
|
||||
"common.submit": "Send inn",
|
||||
"common.save": "Lagre",
|
||||
"common.saving": "Lagrer...",
|
||||
"common.default": "Standard",
|
||||
"common.attachment": "vedlegg",
|
||||
|
||||
"prompt.placeholder.shell": "Skriv inn shell-kommando...",
|
||||
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "ESC for å avslutte",
|
||||
|
||||
"prompt.example.1": "Fiks en TODO i kodebasen",
|
||||
"prompt.example.2": "Hva er teknologistabelen i dette prosjektet?",
|
||||
"prompt.example.3": "Fiks ødelagte tester",
|
||||
"prompt.example.4": "Forklar hvordan autentisering fungerer",
|
||||
"prompt.example.5": "Finn og fiks sikkerhetssårbarheter",
|
||||
"prompt.example.6": "Legg til enhetstester for brukerservicen",
|
||||
"prompt.example.7": "Refaktorer denne funksjonen for bedre lesbarhet",
|
||||
"prompt.example.8": "Hva betyr denne feilen?",
|
||||
"prompt.example.9": "Hjelp meg med å feilsøke dette problemet",
|
||||
"prompt.example.10": "Generer API-dokumentasjon",
|
||||
"prompt.example.11": "Optimaliser databasespørringer",
|
||||
"prompt.example.12": "Legg til inputvalidering",
|
||||
"prompt.example.13": "Lag en ny komponent for...",
|
||||
"prompt.example.14": "Hvordan deployer jeg dette prosjektet?",
|
||||
"prompt.example.15": "Gjennomgå koden min for beste praksis",
|
||||
"prompt.example.16": "Legg til feilhåndtering i denne funksjonen",
|
||||
"prompt.example.17": "Forklar dette regex-mønsteret",
|
||||
"prompt.example.18": "Konverter dette til TypeScript",
|
||||
"prompt.example.19": "Legg til logging i hele kodebasen",
|
||||
"prompt.example.20": "Hvilke avhengigheter er utdaterte?",
|
||||
"prompt.example.21": "Hjelp meg med å skrive et migreringsskript",
|
||||
"prompt.example.22": "Implementer caching for dette endepunktet",
|
||||
"prompt.example.23": "Legg til paginering i denne listen",
|
||||
"prompt.example.24": "Lag en CLI-kommando for...",
|
||||
"prompt.example.25": "Hvordan fungerer miljøvariabler her?",
|
||||
|
||||
"prompt.popover.emptyResults": "Ingen matchende resultater",
|
||||
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
|
||||
"prompt.dropzone.label": "Slipp bilder eller PDF-er her",
|
||||
"prompt.slash.badge.custom": "egendefinert",
|
||||
"prompt.context.active": "aktiv",
|
||||
"prompt.context.includeActiveFile": "Inkluder aktiv fil",
|
||||
"prompt.context.removeActiveFile": "Fjern aktiv fil fra kontekst",
|
||||
"prompt.context.removeFile": "Fjern fil fra kontekst",
|
||||
"prompt.action.attachFile": "Legg ved fil",
|
||||
"prompt.attachment.remove": "Fjern vedlegg",
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stopp",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Liming ikke støttet",
|
||||
"prompt.toast.pasteUnsupported.description": "Kun bilder eller PDF-er kan limes inn her.",
|
||||
"prompt.toast.modelAgentRequired.title": "Velg en agent og modell",
|
||||
"prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Kunne ikke opprette worktree",
|
||||
"prompt.toast.sessionCreateFailed.title": "Kunne ikke opprette sesjon",
|
||||
"prompt.toast.shellSendFailed.title": "Kunne ikke sende shell-kommando",
|
||||
"prompt.toast.commandSendFailed.title": "Kunne ikke sende kommando",
|
||||
"prompt.toast.promptSendFailed.title": "Kunne ikke sende forespørsel",
|
||||
|
||||
"dialog.mcp.title": "MCP-er",
|
||||
"dialog.mcp.description": "{{enabled}} av {{total}} aktivert",
|
||||
"dialog.mcp.empty": "Ingen MCP-er konfigurert",
|
||||
|
||||
"mcp.status.connected": "tilkoblet",
|
||||
"mcp.status.failed": "mislyktes",
|
||||
"mcp.status.needs_auth": "trenger autentisering",
|
||||
"mcp.status.disabled": "deaktivert",
|
||||
|
||||
"dialog.fork.empty": "Ingen meldinger å forgrene fra",
|
||||
|
||||
"dialog.directory.search.placeholder": "Søk etter mapper",
|
||||
"dialog.directory.empty": "Ingen mapper funnet",
|
||||
|
||||
"dialog.server.title": "Servere",
|
||||
"dialog.server.description": "Bytt hvilken OpenCode-server denne appen kobler til.",
|
||||
"dialog.server.search.placeholder": "Søk etter servere",
|
||||
"dialog.server.empty": "Ingen servere ennå",
|
||||
"dialog.server.add.title": "Legg til en server",
|
||||
"dialog.server.add.url": "Server-URL",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "Kunne ikke koble til server",
|
||||
"dialog.server.add.checking": "Sjekker...",
|
||||
"dialog.server.add.button": "Legg til",
|
||||
"dialog.server.default.title": "Standardserver",
|
||||
"dialog.server.default.description":
|
||||
"Koble til denne serveren ved oppstart i stedet for å starte en lokal server. Krever omstart.",
|
||||
"dialog.server.default.none": "Ingen server valgt",
|
||||
"dialog.server.default.set": "Sett gjeldende server som standard",
|
||||
"dialog.server.default.clear": "Tøm",
|
||||
"dialog.server.action.remove": "Fjern server",
|
||||
|
||||
"dialog.project.edit.title": "Rediger prosjekt",
|
||||
"dialog.project.edit.name": "Navn",
|
||||
"dialog.project.edit.icon": "Ikon",
|
||||
"dialog.project.edit.icon.alt": "Prosjektikon",
|
||||
"dialog.project.edit.icon.hint": "Klikk eller dra et bilde",
|
||||
"dialog.project.edit.icon.recommended": "Anbefalt: 128x128px",
|
||||
"dialog.project.edit.color": "Farge",
|
||||
"dialog.project.edit.color.select": "Velg fargen {{color}}",
|
||||
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note": 'Omtrentlig fordeling av input-tokens. "Annet" inkluderer verktøydefinisjoner og overhead.',
|
||||
"context.breakdown.system": "System",
|
||||
"context.breakdown.user": "Bruker",
|
||||
"context.breakdown.assistant": "Assistent",
|
||||
"context.breakdown.tool": "Verktøykall",
|
||||
"context.breakdown.other": "Annet",
|
||||
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Rå meldinger",
|
||||
|
||||
"context.stats.session": "Sesjon",
|
||||
"context.stats.messages": "Meldinger",
|
||||
"context.stats.provider": "Leverandør",
|
||||
"context.stats.model": "Modell",
|
||||
"context.stats.limit": "Kontekstgrense",
|
||||
"context.stats.totalTokens": "Totalt antall tokens",
|
||||
"context.stats.usage": "Forbruk",
|
||||
"context.stats.inputTokens": "Input-tokens",
|
||||
"context.stats.outputTokens": "Output-tokens",
|
||||
"context.stats.reasoningTokens": "Resonnerings-tokens",
|
||||
"context.stats.cacheTokens": "Cache-tokens (les/skriv)",
|
||||
"context.stats.userMessages": "Brukermeldinger",
|
||||
"context.stats.assistantMessages": "Assistentmeldinger",
|
||||
"context.stats.totalCost": "Total kostnad",
|
||||
"context.stats.sessionCreated": "Sesjon opprettet",
|
||||
"context.stats.lastActivity": "Siste aktivitet",
|
||||
|
||||
"context.usage.tokens": "Tokens",
|
||||
"context.usage.usage": "Forbruk",
|
||||
"context.usage.cost": "Kostnad",
|
||||
"context.usage.clickToView": "Klikk for å se kontekst",
|
||||
"context.usage.view": "Se kontekstforbruk",
|
||||
|
||||
"language.en": "Engelsk",
|
||||
"language.zh": "Kinesisk (forenklet)",
|
||||
"language.zht": "Kinesisk (tradisjonell)",
|
||||
"language.ko": "Koreansk",
|
||||
"language.de": "Tysk",
|
||||
"language.es": "Spansk",
|
||||
"language.fr": "Fransk",
|
||||
"language.ja": "Japansk",
|
||||
"language.da": "Dansk",
|
||||
"language.ru": "Russisk",
|
||||
"language.pl": "Polsk",
|
||||
"language.ar": "Arabisk",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Portugisisk (Brasil)",
|
||||
|
||||
"toast.language.title": "Språk",
|
||||
"toast.language.description": "Byttet til {{language}}",
|
||||
|
||||
"toast.theme.title": "Tema byttet",
|
||||
"toast.scheme.title": "Fargevalg",
|
||||
|
||||
"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.model.none.title": "Ingen modell valgt",
|
||||
"toast.model.none.description": "Koble til en leverandør for å oppsummere denne sesjonen",
|
||||
|
||||
"toast.file.loadFailed.title": "Kunne ikke laste fil",
|
||||
|
||||
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til utklippstavlen",
|
||||
"toast.session.share.success.title": "Sesjon delt",
|
||||
"toast.session.share.success.description": "Delings-URL kopiert til utklippstavlen!",
|
||||
"toast.session.share.failed.title": "Kunne ikke dele sesjon",
|
||||
"toast.session.share.failed.description": "Det oppstod en feil under deling av sesjonen",
|
||||
|
||||
"toast.session.unshare.success.title": "Deling av sesjon stoppet",
|
||||
"toast.session.unshare.success.description": "Sesjonen deles ikke lenger!",
|
||||
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling av sesjon",
|
||||
"toast.session.unshare.failed.description": "Det oppstod en feil da delingen av sesjonen skulle stoppes",
|
||||
|
||||
"toast.session.listFailed.title": "Kunne ikke laste sesjoner for {{project}}",
|
||||
|
||||
"toast.update.title": "Oppdatering tilgjengelig",
|
||||
"toast.update.description": "En ny versjon av OpenCode ({{version}}) er nå tilgjengelig for installasjon.",
|
||||
"toast.update.action.installRestart": "Installer og start på nytt",
|
||||
"toast.update.action.notYet": "Ikke nå",
|
||||
|
||||
"error.page.title": "Noe gikk galt",
|
||||
"error.page.description": "Det oppstod en feil under lasting av applikasjonen.",
|
||||
"error.page.details.label": "Feildetaljer",
|
||||
"error.page.action.restart": "Start på nytt",
|
||||
"error.page.action.checking": "Sjekker...",
|
||||
"error.page.action.checkUpdates": "Se etter oppdateringer",
|
||||
"error.page.action.updateTo": "Oppdater til {{version}}",
|
||||
"error.page.report.prefix": "Vennligst rapporter denne feilen til OpenCode-teamet",
|
||||
"error.page.report.discord": "på Discord",
|
||||
"error.page.version": "Versjon: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?",
|
||||
|
||||
"error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?",
|
||||
|
||||
"error.chain.unknown": "Ukjent feil",
|
||||
"error.chain.causedBy": "Forårsaket av:",
|
||||
"error.chain.apiError": "API-feil",
|
||||
"error.chain.status": "Status: {{status}}",
|
||||
"error.chain.retryable": "Kan prøves på nytt: {{retryable}}",
|
||||
"error.chain.responseBody": "Responsinnhold:\n{{body}}",
|
||||
"error.chain.didYouMean": "Mente du: {{suggestions}}",
|
||||
"error.chain.modelNotFound": "Modell ikke funnet: {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "Sjekk leverandør-/modellnavnene i konfigurasjonen din (opencode.json)",
|
||||
"error.chain.mcpFailed": 'MCP-server "{{name}}" mislyktes. Merk at OpenCode ikke støtter MCP-autentisering ennå.',
|
||||
"error.chain.providerAuthFailed": "Leverandørautentisering mislyktes ({{provider}}): {{message}}",
|
||||
"error.chain.providerInitFailed":
|
||||
'Kunne ikke initialisere leverandør "{{provider}}". Sjekk legitimasjon og konfigurasjon.',
|
||||
"error.chain.configJsonInvalid": "Konfigurasjonsfilen på {{path}} er ikke gyldig JSON(C)",
|
||||
"error.chain.configJsonInvalidWithMessage": "Konfigurasjonsfilen på {{path}} er ikke gyldig JSON(C): {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'Mappen "{{dir}}" i {{path}} er ikke gyldig. Gi mappen nytt navn til "{{suggestion}}" eller fjern den. Dette er en vanlig skrivefeil.',
|
||||
"error.chain.configFrontmatterError": "Kunne ikke analysere frontmatter i {{path}}:\n{{message}}",
|
||||
"error.chain.configInvalid": "Konfigurasjonsfilen på {{path}} er ugyldig",
|
||||
"error.chain.configInvalidWithMessage": "Konfigurasjonsfilen på {{path}} er ugyldig: {{message}}",
|
||||
|
||||
"notification.permission.title": "Tillatelse påkrevd",
|
||||
"notification.permission.description": "{{sessionTitle}} i {{projectName}} trenger tillatelse",
|
||||
"notification.question.title": "Spørsmål",
|
||||
"notification.question.description": "{{sessionTitle}} i {{projectName}} har et spørsmål",
|
||||
"notification.action.goToSession": "Gå til sesjon",
|
||||
|
||||
"notification.session.responseReady.title": "Svar klart",
|
||||
"notification.session.error.title": "Sesjonsfeil",
|
||||
"notification.session.error.fallbackDescription": "Det oppstod en feil",
|
||||
|
||||
"home.recentProjects": "Nylige prosjekter",
|
||||
"home.empty.title": "Ingen nylige prosjekter",
|
||||
"home.empty.description": "Kom i gang ved å åpne et lokalt prosjekt",
|
||||
|
||||
"session.tab.session": "Sesjon",
|
||||
"session.tab.review": "Gjennomgang",
|
||||
"session.tab.context": "Kontekst",
|
||||
"session.panel.reviewAndFiles": "Gjennomgang og filer",
|
||||
"session.review.filesChanged": "{{count}} filer endret",
|
||||
"session.review.loadingChanges": "Laster endringer...",
|
||||
"session.review.empty": "Ingen endringer i denne sesjonen ennå",
|
||||
"session.messages.renderEarlier": "Vis tidligere meldinger",
|
||||
"session.messages.loadingEarlier": "Laster inn tidligere meldinger...",
|
||||
"session.messages.loadEarlier": "Last inn tidligere meldinger",
|
||||
"session.messages.loading": "Laster meldinger...",
|
||||
"session.messages.jumpToLatest": "Hopp til nyeste",
|
||||
|
||||
"session.context.addToContext": "Legg til {{selection}} i kontekst",
|
||||
|
||||
"session.new.worktree.main": "Hovedgren",
|
||||
"session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})",
|
||||
"session.new.worktree.create": "Opprett nytt worktree",
|
||||
"session.new.lastModified": "Sist endret",
|
||||
|
||||
"session.header.search.placeholder": "Søk i {{project}}",
|
||||
"session.header.searchFiles": "Søk etter filer",
|
||||
|
||||
"session.share.popover.title": "Publiser på nett",
|
||||
"session.share.popover.description.shared":
|
||||
"Denne sesjonen er offentlig på nettet. Den er tilgjengelig for alle med lenken.",
|
||||
"session.share.popover.description.unshared":
|
||||
"Del sesjonen offentlig på nettet. Den vil være tilgjengelig for alle med lenken.",
|
||||
"session.share.action.share": "Del",
|
||||
"session.share.action.publish": "Publiser",
|
||||
"session.share.action.publishing": "Publiserer...",
|
||||
"session.share.action.unpublish": "Avpubliser",
|
||||
"session.share.action.unpublishing": "Avpubliserer...",
|
||||
"session.share.action.view": "Vis",
|
||||
"session.share.copy.copied": "Kopiert",
|
||||
"session.share.copy.copyLink": "Kopier lenke",
|
||||
|
||||
"lsp.tooltip.none": "Ingen LSP-servere",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "Laster prompt...",
|
||||
"terminal.loading": "Laster terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Lukk terminal",
|
||||
"terminal.connectionLost.title": "Tilkobling mistet",
|
||||
"terminal.connectionLost.description":
|
||||
"Terminalforbindelsen ble avbrutt. Dette kan skje når serveren starter på nytt.",
|
||||
|
||||
"common.closeTab": "Lukk fane",
|
||||
"common.dismiss": "Avvis",
|
||||
"common.requestFailed": "Forespørsel mislyktes",
|
||||
"common.moreOptions": "Flere alternativer",
|
||||
"common.learnMore": "Lær mer",
|
||||
"common.rename": "Gi nytt navn",
|
||||
"common.reset": "Tilbakestill",
|
||||
"common.delete": "Slett",
|
||||
"common.close": "Lukk",
|
||||
"common.edit": "Rediger",
|
||||
"common.loadMore": "Last flere",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Veksle meny",
|
||||
"sidebar.nav.projectsAndSessions": "Prosjekter og sesjoner",
|
||||
"sidebar.settings": "Innstillinger",
|
||||
"sidebar.help": "Hjelp",
|
||||
"sidebar.workspaces.enable": "Aktiver arbeidsområder",
|
||||
"sidebar.workspaces.disable": "Deaktiver arbeidsområder",
|
||||
"sidebar.gettingStarted.title": "Kom i gang",
|
||||
"sidebar.gettingStarted.line1": "OpenCode inkluderer gratis modeller så du kan starte umiddelbart.",
|
||||
"sidebar.gettingStarted.line2": "Koble til en leverandør for å bruke modeller, inkl. Claude, GPT, Gemini osv.",
|
||||
"sidebar.project.recentSessions": "Nylige sesjoner",
|
||||
"sidebar.project.viewAllSessions": "Vis alle sesjoner",
|
||||
|
||||
"settings.section.desktop": "Skrivebord",
|
||||
"settings.tab.general": "Generelt",
|
||||
"settings.tab.shortcuts": "Snarveier",
|
||||
|
||||
"settings.general.section.appearance": "Utseende",
|
||||
"settings.general.section.notifications": "Systemvarsler",
|
||||
"settings.general.section.sounds": "Lydeffekter",
|
||||
|
||||
"settings.general.row.language.title": "Språk",
|
||||
"settings.general.row.language.description": "Endre visningsspråket for OpenCode",
|
||||
"settings.general.row.appearance.title": "Utseende",
|
||||
"settings.general.row.appearance.description": "Tilpass hvordan OpenCode ser ut på enheten din",
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.",
|
||||
"settings.general.row.font.title": "Skrift",
|
||||
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
|
||||
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Vis systemvarsel når agenten er ferdig eller trenger oppmerksomhet",
|
||||
"settings.general.notifications.permissions.title": "Tillatelser",
|
||||
"settings.general.notifications.permissions.description": "Vis systemvarsel når en tillatelse er påkrevd",
|
||||
"settings.general.notifications.errors.title": "Feil",
|
||||
"settings.general.notifications.errors.description": "Vis systemvarsel når det oppstår en feil",
|
||||
|
||||
"settings.general.sounds.agent.title": "Agent",
|
||||
"settings.general.sounds.agent.description": "Spill av lyd når agenten er ferdig eller trenger oppmerksomhet",
|
||||
"settings.general.sounds.permissions.title": "Tillatelser",
|
||||
"settings.general.sounds.permissions.description": "Spill av lyd når en tillatelse er påkrevd",
|
||||
"settings.general.sounds.errors.title": "Feil",
|
||||
"settings.general.sounds.errors.description": "Spill av lyd når det oppstår en feil",
|
||||
|
||||
"settings.shortcuts.title": "Tastatursnarveier",
|
||||
"settings.shortcuts.reset.button": "Tilbakestill til standard",
|
||||
"settings.shortcuts.reset.toast.title": "Snarveier tilbakestilt",
|
||||
"settings.shortcuts.reset.toast.description": "Tastatursnarveier er tilbakestilt til standard.",
|
||||
"settings.shortcuts.conflict.title": "Snarvei allerede i bruk",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} er allerede tilordnet til {{titles}}.",
|
||||
"settings.shortcuts.unassigned": "Ikke tilordnet",
|
||||
"settings.shortcuts.pressKeys": "Trykk taster",
|
||||
"settings.shortcuts.search.placeholder": "Søk etter snarveier",
|
||||
"settings.shortcuts.search.empty": "Ingen snarveier funnet",
|
||||
|
||||
"settings.shortcuts.group.general": "Generelt",
|
||||
"settings.shortcuts.group.session": "Sesjon",
|
||||
"settings.shortcuts.group.navigation": "Navigasjon",
|
||||
"settings.shortcuts.group.modelAndAgent": "Modell og agent",
|
||||
"settings.shortcuts.group.terminal": "Terminal",
|
||||
"settings.shortcuts.group.prompt": "Prompt",
|
||||
|
||||
"settings.providers.title": "Leverandører",
|
||||
"settings.providers.description": "Leverandørinnstillinger vil kunne konfigureres her.",
|
||||
"settings.models.title": "Modeller",
|
||||
"settings.models.description": "Modellinnstillinger vil kunne konfigureres her.",
|
||||
"settings.agents.title": "Agenter",
|
||||
"settings.agents.description": "Agentinnstillinger vil kunne konfigureres her.",
|
||||
"settings.commands.title": "Kommandoer",
|
||||
"settings.commands.description": "Kommandoinnstillinger vil kunne konfigureres her.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP-innstillinger vil kunne konfigureres her.",
|
||||
|
||||
"settings.permissions.title": "Tillatelser",
|
||||
"settings.permissions.description": "Kontroller hvilke verktøy serveren kan bruke som standard.",
|
||||
"settings.permissions.section.tools": "Verktøy",
|
||||
"settings.permissions.toast.updateFailed.title": "Kunne ikke oppdatere tillatelser",
|
||||
|
||||
"settings.permissions.action.allow": "Tillat",
|
||||
"settings.permissions.action.ask": "Spør",
|
||||
"settings.permissions.action.deny": "Avslå",
|
||||
|
||||
"settings.permissions.tool.read.title": "Les",
|
||||
"settings.permissions.tool.read.description": "Lesing av en fil (matcher filbanen)",
|
||||
"settings.permissions.tool.edit.title": "Rediger",
|
||||
"settings.permissions.tool.edit.description":
|
||||
"Endre filer, inkludert redigeringer, skriving, patcher og multi-redigeringer",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Match filer ved hjelp av glob-mønstre",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description": "Søk i filinnhold ved hjelp av regulære uttrykk",
|
||||
"settings.permissions.tool.list.title": "Liste",
|
||||
"settings.permissions.tool.list.description": "List filer i en mappe",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "Kjør shell-kommandoer",
|
||||
"settings.permissions.tool.task.title": "Oppgave",
|
||||
"settings.permissions.tool.task.description": "Start underagenter",
|
||||
"settings.permissions.tool.skill.title": "Ferdighet",
|
||||
"settings.permissions.tool.skill.description": "Last en ferdighet etter navn",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "Kjør språkserverforespørsler",
|
||||
"settings.permissions.tool.todoread.title": "Les gjøremål",
|
||||
"settings.permissions.tool.todoread.description": "Les gjøremålslisten",
|
||||
"settings.permissions.tool.todowrite.title": "Skriv gjøremål",
|
||||
"settings.permissions.tool.todowrite.description": "Oppdater gjøremålslisten",
|
||||
"settings.permissions.tool.webfetch.title": "Webhenting",
|
||||
"settings.permissions.tool.webfetch.description": "Hent innhold fra en URL",
|
||||
"settings.permissions.tool.websearch.title": "Websøk",
|
||||
"settings.permissions.tool.websearch.description": "Søk på nettet",
|
||||
"settings.permissions.tool.codesearch.title": "Kodesøk",
|
||||
"settings.permissions.tool.codesearch.description": "Søk etter kode på nettet",
|
||||
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
|
||||
"settings.permissions.tool.external_directory.description": "Få tilgang til filer utenfor prosjektmappen",
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "Oppdager gjentatte verktøykall med identisk input",
|
||||
|
||||
"workspace.new": "Nytt arbeidsområde",
|
||||
"workspace.type.local": "lokal",
|
||||
"workspace.type.sandbox": "sandkasse",
|
||||
"workspace.create.failed.title": "Kunne ikke opprette arbeidsområde",
|
||||
"workspace.delete.failed.title": "Kunne ikke slette arbeidsområde",
|
||||
"workspace.resetting.title": "Tilbakestiller arbeidsområde",
|
||||
"workspace.resetting.description": "Dette kan ta et minutt.",
|
||||
"workspace.reset.failed.title": "Kunne ikke tilbakestille arbeidsområde",
|
||||
"workspace.reset.success.title": "Arbeidsområde tilbakestilt",
|
||||
"workspace.reset.success.description": "Arbeidsområdet samsvarer nå med standardgrenen.",
|
||||
"workspace.status.checking": "Sjekker for ikke-sammenslåtte endringer...",
|
||||
"workspace.status.error": "Kunne ikke bekrefte git-status.",
|
||||
"workspace.status.clean": "Ingen ikke-sammenslåtte endringer oppdaget.",
|
||||
"workspace.status.dirty": "Ikke-sammenslåtte endringer oppdaget i dette arbeidsområdet.",
|
||||
"workspace.delete.title": "Slett arbeidsområde",
|
||||
"workspace.delete.confirm": 'Slette arbeidsområdet "{{name}}"?',
|
||||
"workspace.delete.button": "Slett arbeidsområde",
|
||||
"workspace.reset.title": "Tilbakestill arbeidsområde",
|
||||
"workspace.reset.confirm": 'Tilbakestille arbeidsområdet "{{name}}"?',
|
||||
"workspace.reset.button": "Tilbakestill arbeidsområde",
|
||||
"workspace.reset.archived.none": "Ingen aktive sesjoner vil bli arkivert.",
|
||||
"workspace.reset.archived.one": "1 sesjon vil bli arkivert.",
|
||||
"workspace.reset.archived.many": "{{count}} sesjoner vil bli arkivert.",
|
||||
"workspace.reset.note": "Dette vil tilbakestille arbeidsområdet til å samsvare med standardgrenen.",
|
||||
} satisfies Partial<Record<Keys, string>>
|
||||
@@ -88,6 +88,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Inne",
|
||||
"dialog.provider.tag.recommended": "Zalecane",
|
||||
"dialog.provider.anthropic.note": "Połącz z Claude Pro/Max lub kluczem API",
|
||||
"dialog.provider.openai.note": "Połącz z ChatGPT Pro/Plus lub kluczem API",
|
||||
"dialog.provider.copilot.note": "Połącz z Copilot lub kluczem API",
|
||||
|
||||
"dialog.model.select.title": "Wybierz model",
|
||||
"dialog.model.search.placeholder": "Szukaj modeli",
|
||||
@@ -153,6 +155,7 @@ export const dict = {
|
||||
"model.tooltip.context": "Limit kontekstu {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Szukaj",
|
||||
"common.goBack": "Wstecz",
|
||||
"common.loading": "Ładowanie",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Anuluj",
|
||||
@@ -199,7 +202,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "własne",
|
||||
"prompt.context.active": "aktywny",
|
||||
"prompt.context.includeActiveFile": "Dołącz aktywny plik",
|
||||
"prompt.context.removeActiveFile": "Usuń aktywny plik z kontekstu",
|
||||
"prompt.context.removeFile": "Usuń plik z kontekstu",
|
||||
"prompt.action.attachFile": "Załącz plik",
|
||||
"prompt.attachment.remove": "Usuń załącznik",
|
||||
"prompt.action.send": "Wyślij",
|
||||
"prompt.action.stop": "Zatrzymaj",
|
||||
|
||||
@@ -243,6 +249,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "Nie wybrano serwera",
|
||||
"dialog.server.default.set": "Ustaw bieżący serwer jako domyślny",
|
||||
"dialog.server.default.clear": "Wyczyść",
|
||||
"dialog.server.action.remove": "Usuń serwer",
|
||||
|
||||
"dialog.project.edit.title": "Edytuj projekt",
|
||||
"dialog.project.edit.name": "Nazwa",
|
||||
@@ -251,6 +258,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "Kliknij lub przeciągnij obraz",
|
||||
"dialog.project.edit.icon.recommended": "Zalecane: 128x128px",
|
||||
"dialog.project.edit.color": "Kolor",
|
||||
"dialog.project.edit.color.select": "Wybierz kolor {{color}}",
|
||||
|
||||
"context.breakdown.title": "Podział kontekstu",
|
||||
"context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.',
|
||||
@@ -284,6 +292,7 @@ export const dict = {
|
||||
"context.usage.usage": "Użycie",
|
||||
"context.usage.cost": "Koszt",
|
||||
"context.usage.clickToView": "Kliknij, aby zobaczyć kontekst",
|
||||
"context.usage.view": "Pokaż użycie kontekstu",
|
||||
|
||||
"language.en": "Angielski",
|
||||
"language.zh": "Chiński",
|
||||
@@ -294,6 +303,10 @@ export const dict = {
|
||||
"language.ja": "Japoński",
|
||||
"language.da": "Duński",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Rosyjski",
|
||||
"language.ar": "Arabski",
|
||||
"language.no": "Norweski",
|
||||
"language.br": "Portugalski (Brazylia)",
|
||||
|
||||
"toast.language.title": "Język",
|
||||
"toast.language.description": "Przełączono na {{language}}",
|
||||
@@ -384,6 +397,7 @@ export const dict = {
|
||||
"session.tab.session": "Sesja",
|
||||
"session.tab.review": "Przegląd",
|
||||
"session.tab.context": "Kontekst",
|
||||
"session.panel.reviewAndFiles": "Przegląd i pliki",
|
||||
"session.review.filesChanged": "Zmieniono {{count}} plików",
|
||||
"session.review.loadingChanges": "Ładowanie zmian...",
|
||||
"session.review.empty": "Brak zmian w tej sesji",
|
||||
@@ -401,6 +415,7 @@ export const dict = {
|
||||
"session.new.lastModified": "Ostatnio zmodyfikowano",
|
||||
|
||||
"session.header.search.placeholder": "Szukaj {{project}}",
|
||||
"session.header.searchFiles": "Szukaj plików",
|
||||
|
||||
"session.share.popover.title": "Opublikuj w sieci",
|
||||
"session.share.popover.description.shared":
|
||||
@@ -423,6 +438,7 @@ export const dict = {
|
||||
"terminal.loading": "Ładowanie terminala...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Zamknij terminal",
|
||||
"terminal.connectionLost.title": "Utracono połączenie",
|
||||
"terminal.connectionLost.description":
|
||||
"Połączenie z terminalem zostało przerwane. Może się to zdarzyć przy restarcie serwera.",
|
||||
@@ -434,6 +450,7 @@ export const dict = {
|
||||
"common.learnMore": "Dowiedz się więcej",
|
||||
"common.rename": "Zmień nazwę",
|
||||
"common.reset": "Resetuj",
|
||||
"common.archive": "Archiwizuj",
|
||||
"common.delete": "Usuń",
|
||||
"common.close": "Zamknij",
|
||||
"common.edit": "Edytuj",
|
||||
@@ -441,6 +458,7 @@ export const dict = {
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Przełącz menu",
|
||||
"sidebar.nav.projectsAndSessions": "Projekty i sesje",
|
||||
"sidebar.settings": "Ustawienia",
|
||||
"sidebar.help": "Pomoc",
|
||||
"sidebar.workspaces.enable": "Włącz przestrzenie robocze",
|
||||
@@ -611,6 +629,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Zapętlenie",
|
||||
"settings.permissions.tool.doom_loop.description": "Wykrywanie powtarzających się wywołań narzędzi (doom loop)",
|
||||
|
||||
"session.delete.failed.title": "Nie udało się usunąć sesji",
|
||||
"session.delete.title": "Usuń sesję",
|
||||
"session.delete.confirm": 'Usunąć sesję "{{name}}"?',
|
||||
"session.delete.button": "Usuń sesję",
|
||||
|
||||
"workspace.new": "Nowa przestrzeń robocza",
|
||||
"workspace.type.local": "lokalna",
|
||||
"workspace.type.sandbox": "piaskownica",
|
||||
|
||||
@@ -88,6 +88,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "Другие",
|
||||
"dialog.provider.tag.recommended": "Рекомендуемые",
|
||||
"dialog.provider.anthropic.note": "Подключитесь с помощью Claude Pro/Max или API ключа",
|
||||
"dialog.provider.openai.note": "Подключитесь с помощью ChatGPT Pro/Plus или API ключа",
|
||||
"dialog.provider.copilot.note": "Подключитесь с помощью Copilot или API ключа",
|
||||
|
||||
"dialog.model.select.title": "Выбрать модель",
|
||||
"dialog.model.search.placeholder": "Поиск моделей",
|
||||
@@ -153,6 +155,7 @@ export const dict = {
|
||||
"model.tooltip.context": "Лимит контекста {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Поиск",
|
||||
"common.goBack": "Назад",
|
||||
"common.loading": "Загрузка",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Отмена",
|
||||
@@ -199,7 +202,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "своё",
|
||||
"prompt.context.active": "активно",
|
||||
"prompt.context.includeActiveFile": "Включить активный файл",
|
||||
"prompt.context.removeActiveFile": "Удалить активный файл из контекста",
|
||||
"prompt.context.removeFile": "Удалить файл из контекста",
|
||||
"prompt.action.attachFile": "Прикрепить файл",
|
||||
"prompt.attachment.remove": "Удалить вложение",
|
||||
"prompt.action.send": "Отправить",
|
||||
"prompt.action.stop": "Остановить",
|
||||
|
||||
@@ -243,6 +249,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "Сервер не выбран",
|
||||
"dialog.server.default.set": "Установить текущий сервер по умолчанию",
|
||||
"dialog.server.default.clear": "Очистить",
|
||||
"dialog.server.action.remove": "Удалить сервер",
|
||||
|
||||
"dialog.project.edit.title": "Редактировать проект",
|
||||
"dialog.project.edit.name": "Название",
|
||||
@@ -251,6 +258,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "Нажмите или перетащите изображение",
|
||||
"dialog.project.edit.icon.recommended": "Рекомендуется: 128x128px",
|
||||
"dialog.project.edit.color": "Цвет",
|
||||
"dialog.project.edit.color.select": "Выбрать цвет {{color}}",
|
||||
|
||||
"context.breakdown.title": "Разбивка контекста",
|
||||
"context.breakdown.note":
|
||||
@@ -285,6 +293,7 @@ export const dict = {
|
||||
"context.usage.usage": "Использование",
|
||||
"context.usage.cost": "Стоимость",
|
||||
"context.usage.clickToView": "Нажмите для просмотра контекста",
|
||||
"context.usage.view": "Показать использование контекста",
|
||||
|
||||
"language.en": "Английский",
|
||||
"language.zh": "Китайский",
|
||||
@@ -295,6 +304,9 @@ export const dict = {
|
||||
"language.ja": "Японский",
|
||||
"language.da": "Датский",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "Арабский",
|
||||
"language.no": "Норвежский",
|
||||
"language.br": "Португальский (Бразилия)",
|
||||
|
||||
"toast.language.title": "Язык",
|
||||
"toast.language.description": "Переключено на {{language}}",
|
||||
@@ -386,6 +398,7 @@ export const dict = {
|
||||
"session.tab.session": "Сессия",
|
||||
"session.tab.review": "Обзор",
|
||||
"session.tab.context": "Контекст",
|
||||
"session.panel.reviewAndFiles": "Обзор и файлы",
|
||||
"session.review.filesChanged": "{{count}} файлов изменено",
|
||||
"session.review.loadingChanges": "Загрузка изменений...",
|
||||
"session.review.empty": "Изменений в этой сессии пока нет",
|
||||
@@ -403,6 +416,7 @@ export const dict = {
|
||||
"session.new.lastModified": "Последнее изменение",
|
||||
|
||||
"session.header.search.placeholder": "Поиск {{project}}",
|
||||
"session.header.searchFiles": "Поиск файлов",
|
||||
|
||||
"session.share.popover.title": "Опубликовать в интернете",
|
||||
"session.share.popover.description.shared":
|
||||
@@ -425,6 +439,7 @@ export const dict = {
|
||||
"terminal.loading": "Загрузка терминала...",
|
||||
"terminal.title": "Терминал",
|
||||
"terminal.title.numbered": "Терминал {{number}}",
|
||||
"terminal.close": "Закрыть терминал",
|
||||
"terminal.connectionLost.title": "Соединение потеряно",
|
||||
"terminal.connectionLost.description":
|
||||
"Соединение с терминалом прервано. Это может произойти при перезапуске сервера.",
|
||||
@@ -436,6 +451,7 @@ export const dict = {
|
||||
"common.learnMore": "Подробнее",
|
||||
"common.rename": "Переименовать",
|
||||
"common.reset": "Сбросить",
|
||||
"common.archive": "Архивировать",
|
||||
"common.delete": "Удалить",
|
||||
"common.close": "Закрыть",
|
||||
"common.edit": "Редактировать",
|
||||
@@ -443,6 +459,7 @@ export const dict = {
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Переключить меню",
|
||||
"sidebar.nav.projectsAndSessions": "Проекты и сессии",
|
||||
"sidebar.settings": "Настройки",
|
||||
"sidebar.help": "Помощь",
|
||||
"sidebar.workspaces.enable": "Включить рабочие пространства",
|
||||
@@ -615,6 +632,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "Обнаружение повторных вызовов инструментов с одинаковым вводом",
|
||||
|
||||
"session.delete.failed.title": "Не удалось удалить сессию",
|
||||
"session.delete.title": "Удалить сессию",
|
||||
"session.delete.confirm": 'Удалить сессию "{{name}}"?',
|
||||
"session.delete.button": "Удалить сессию",
|
||||
|
||||
"workspace.new": "Новое рабочее пространство",
|
||||
"workspace.type.local": "локальное",
|
||||
"workspace.type.sandbox": "песочница",
|
||||
|
||||
@@ -90,6 +90,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "其他",
|
||||
"dialog.provider.tag.recommended": "推荐",
|
||||
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
|
||||
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
|
||||
"dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接",
|
||||
|
||||
"dialog.model.select.title": "选择模型",
|
||||
"dialog.model.search.placeholder": "搜索模型",
|
||||
@@ -136,6 +138,7 @@ export const dict = {
|
||||
"model.tag.latest": "最新",
|
||||
|
||||
"common.search.placeholder": "搜索",
|
||||
"common.goBack": "返回",
|
||||
"common.loading": "加载中",
|
||||
"common.cancel": "取消",
|
||||
"common.submit": "提交",
|
||||
@@ -181,7 +184,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "自定义",
|
||||
"prompt.context.active": "当前",
|
||||
"prompt.context.includeActiveFile": "包含当前文件",
|
||||
"prompt.context.removeActiveFile": "从上下文移除活动文件",
|
||||
"prompt.context.removeFile": "从上下文移除文件",
|
||||
"prompt.action.attachFile": "附加文件",
|
||||
"prompt.attachment.remove": "移除附件",
|
||||
"prompt.action.send": "发送",
|
||||
"prompt.action.stop": "停止",
|
||||
|
||||
@@ -224,6 +230,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "未选择服务器",
|
||||
"dialog.server.default.set": "将当前服务器设为默认",
|
||||
"dialog.server.default.clear": "清除",
|
||||
"dialog.server.action.remove": "移除服务器",
|
||||
|
||||
"dialog.project.edit.title": "编辑项目",
|
||||
"dialog.project.edit.name": "名称",
|
||||
@@ -232,6 +239,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "点击或拖拽图片",
|
||||
"dialog.project.edit.icon.recommended": "建议:128x128px",
|
||||
"dialog.project.edit.color": "颜色",
|
||||
"dialog.project.edit.color.select": "选择{{color}}颜色",
|
||||
|
||||
"context.breakdown.title": "上下文拆分",
|
||||
"context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。",
|
||||
@@ -265,6 +273,7 @@ export const dict = {
|
||||
"context.usage.usage": "使用率",
|
||||
"context.usage.cost": "成本",
|
||||
"context.usage.clickToView": "点击查看上下文",
|
||||
"context.usage.view": "查看上下文用量",
|
||||
|
||||
"language.en": "英语",
|
||||
"language.zh": "简体中文",
|
||||
@@ -277,6 +286,9 @@ export const dict = {
|
||||
"language.da": "丹麦语",
|
||||
"language.ru": "俄语",
|
||||
"language.pl": "波兰语",
|
||||
"language.ar": "阿拉伯语",
|
||||
"language.no": "挪威语",
|
||||
"language.br": "葡萄牙语(巴西)",
|
||||
|
||||
"toast.language.title": "语言",
|
||||
"toast.language.description": "已切换到{{language}}",
|
||||
@@ -364,6 +376,7 @@ export const dict = {
|
||||
"session.tab.session": "会话",
|
||||
"session.tab.review": "审查",
|
||||
"session.tab.context": "上下文",
|
||||
"session.panel.reviewAndFiles": "审查和文件",
|
||||
"session.review.filesChanged": "{{count}} 个文件变更",
|
||||
"session.review.loadingChanges": "正在加载更改...",
|
||||
"session.review.empty": "此会话暂无更改",
|
||||
@@ -380,6 +393,7 @@ export const dict = {
|
||||
"session.new.lastModified": "最后修改",
|
||||
|
||||
"session.header.search.placeholder": "搜索 {{project}}",
|
||||
"session.header.searchFiles": "搜索文件",
|
||||
|
||||
"session.share.popover.title": "发布到网页",
|
||||
"session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。",
|
||||
@@ -400,6 +414,7 @@ export const dict = {
|
||||
"terminal.loading": "正在加载终端...",
|
||||
"terminal.title": "终端",
|
||||
"terminal.title.numbered": "终端 {{number}}",
|
||||
"terminal.close": "关闭终端",
|
||||
|
||||
"common.closeTab": "关闭标签页",
|
||||
"common.dismiss": "忽略",
|
||||
@@ -408,11 +423,13 @@ export const dict = {
|
||||
"common.learnMore": "了解更多",
|
||||
"common.rename": "重命名",
|
||||
"common.reset": "重置",
|
||||
"common.archive": "归档",
|
||||
"common.delete": "删除",
|
||||
"common.close": "关闭",
|
||||
"common.edit": "编辑",
|
||||
"common.loadMore": "加载更多",
|
||||
|
||||
"sidebar.nav.projectsAndSessions": "项目和会话",
|
||||
"sidebar.settings": "设置",
|
||||
"sidebar.help": "帮助",
|
||||
"sidebar.workspaces.enable": "启用工作区",
|
||||
@@ -525,6 +542,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用",
|
||||
|
||||
"session.delete.failed.title": "删除会话失败",
|
||||
"session.delete.title": "删除会话",
|
||||
"session.delete.confirm": '删除会话 "{{name}}"?',
|
||||
"session.delete.button": "删除会话",
|
||||
|
||||
"workspace.new": "新建工作区",
|
||||
"workspace.type.local": "本地",
|
||||
"workspace.type.sandbox": "沙盒",
|
||||
|
||||
@@ -90,6 +90,8 @@ export const dict = {
|
||||
"dialog.provider.group.other": "其他",
|
||||
"dialog.provider.tag.recommended": "推薦",
|
||||
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 金鑰連線",
|
||||
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 金鑰連線",
|
||||
"dialog.provider.copilot.note": "使用 Copilot 或 API 金鑰連線",
|
||||
|
||||
"dialog.model.select.title": "選擇模型",
|
||||
"dialog.model.search.placeholder": "搜尋模型",
|
||||
@@ -138,6 +140,7 @@ export const dict = {
|
||||
"model.tag.latest": "最新",
|
||||
|
||||
"common.search.placeholder": "搜尋",
|
||||
"common.goBack": "返回",
|
||||
"common.loading": "載入中",
|
||||
"common.cancel": "取消",
|
||||
"common.submit": "提交",
|
||||
@@ -183,7 +186,10 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "自訂",
|
||||
"prompt.context.active": "作用中",
|
||||
"prompt.context.includeActiveFile": "包含作用中檔案",
|
||||
"prompt.context.removeActiveFile": "從上下文移除目前檔案",
|
||||
"prompt.context.removeFile": "從上下文移除檔案",
|
||||
"prompt.action.attachFile": "附加檔案",
|
||||
"prompt.attachment.remove": "移除附件",
|
||||
"prompt.action.send": "傳送",
|
||||
"prompt.action.stop": "停止",
|
||||
|
||||
@@ -226,6 +232,7 @@ export const dict = {
|
||||
"dialog.server.default.none": "未選擇伺服器",
|
||||
"dialog.server.default.set": "將目前伺服器設為預設",
|
||||
"dialog.server.default.clear": "清除",
|
||||
"dialog.server.action.remove": "移除伺服器",
|
||||
|
||||
"dialog.project.edit.title": "編輯專案",
|
||||
"dialog.project.edit.name": "名稱",
|
||||
@@ -234,6 +241,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.hint": "點擊或拖曳圖片",
|
||||
"dialog.project.edit.icon.recommended": "建議:128x128px",
|
||||
"dialog.project.edit.color": "顏色",
|
||||
"dialog.project.edit.color.select": "選擇{{color}}顏色",
|
||||
|
||||
"context.breakdown.title": "上下文拆分",
|
||||
"context.breakdown.note": "輸入 token 的大致拆分。「其他」包含工具定義和額外開銷。",
|
||||
@@ -267,11 +275,16 @@ export const dict = {
|
||||
"context.usage.usage": "使用量",
|
||||
"context.usage.cost": "成本",
|
||||
"context.usage.clickToView": "點擊查看上下文",
|
||||
"context.usage.view": "檢視上下文用量",
|
||||
|
||||
"language.en": "英語",
|
||||
"language.zh": "簡體中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "韓語",
|
||||
"language.ru": "俄語",
|
||||
"language.ar": "阿拉伯語",
|
||||
"language.no": "挪威語",
|
||||
"language.br": "葡萄牙語(巴西)",
|
||||
|
||||
"toast.language.title": "語言",
|
||||
"toast.language.description": "已切換到 {{language}}",
|
||||
@@ -359,6 +372,7 @@ export const dict = {
|
||||
"session.tab.session": "工作階段",
|
||||
"session.tab.review": "審查",
|
||||
"session.tab.context": "上下文",
|
||||
"session.panel.reviewAndFiles": "審查與檔案",
|
||||
"session.review.filesChanged": "{{count}} 個檔案變更",
|
||||
"session.review.loadingChanges": "正在載入變更...",
|
||||
"session.review.empty": "此工作階段暫無變更",
|
||||
@@ -375,6 +389,7 @@ export const dict = {
|
||||
"session.new.lastModified": "最後修改",
|
||||
|
||||
"session.header.search.placeholder": "搜尋 {{project}}",
|
||||
"session.header.searchFiles": "搜尋檔案",
|
||||
|
||||
"session.share.popover.title": "發佈到網頁",
|
||||
"session.share.popover.description.shared": "此工作階段已在網頁上公開。任何擁有連結的人都可以存取。",
|
||||
@@ -395,6 +410,7 @@ export const dict = {
|
||||
"terminal.loading": "正在載入終端機...",
|
||||
"terminal.title": "終端機",
|
||||
"terminal.title.numbered": "終端機 {{number}}",
|
||||
"terminal.close": "關閉終端機",
|
||||
|
||||
"common.closeTab": "關閉標籤頁",
|
||||
"common.dismiss": "忽略",
|
||||
@@ -403,11 +419,13 @@ export const dict = {
|
||||
"common.learnMore": "深入了解",
|
||||
"common.rename": "重新命名",
|
||||
"common.reset": "重設",
|
||||
"common.archive": "封存",
|
||||
"common.delete": "刪除",
|
||||
"common.close": "關閉",
|
||||
"common.edit": "編輯",
|
||||
"common.loadMore": "載入更多",
|
||||
|
||||
"sidebar.nav.projectsAndSessions": "專案與工作階段",
|
||||
"sidebar.settings": "設定",
|
||||
"sidebar.help": "說明",
|
||||
"sidebar.workspaces.enable": "啟用工作區",
|
||||
@@ -520,6 +538,11 @@ export const dict = {
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "偵測具有相同輸入的重複工具呼叫",
|
||||
|
||||
"session.delete.failed.title": "刪除工作階段失敗",
|
||||
"session.delete.title": "刪除工作階段",
|
||||
"session.delete.confirm": '刪除工作階段 "{{name}}"?',
|
||||
"session.delete.button": "刪除工作階段",
|
||||
|
||||
"workspace.new": "新增工作區",
|
||||
"workspace.type.local": "本地",
|
||||
"workspace.type.sandbox": "沙盒",
|
||||
|
||||
@@ -29,3 +29,29 @@
|
||||
*[data-tauri-drag-region] {
|
||||
app-region: drag;
|
||||
}
|
||||
|
||||
.session-scroller::-webkit-scrollbar {
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
}
|
||||
|
||||
.session-scroller::-webkit-scrollbar-track {
|
||||
background: transparent !important;
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
.session-scroller::-webkit-scrollbar-thumb {
|
||||
background: var(--border-weak-base) !important;
|
||||
border-radius: 5px !important;
|
||||
border: 3px solid transparent !important;
|
||||
background-clip: padding-box !important;
|
||||
}
|
||||
|
||||
.session-scroller::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-weak-base) !important;
|
||||
}
|
||||
|
||||
.session-scroller {
|
||||
scrollbar-width: thin !important;
|
||||
scrollbar-color: var(--border-weak-base) transparent !important;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function Layout(props: ParentProps) {
|
||||
return base64Decode(params.dir!)
|
||||
})
|
||||
return (
|
||||
<Show when={params.dir} keyed>
|
||||
<Show when={params.dir}>
|
||||
<SDKProvider directory={directory()}>
|
||||
<SyncProvider>
|
||||
{iife(() => {
|
||||
|
||||
@@ -56,6 +56,7 @@ import { usePermission } from "@/context/permission"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { playSound, soundSrc } from "@/utils/sound"
|
||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
@@ -332,6 +333,18 @@ export default function Layout(props: ParentProps) {
|
||||
const cooldownMs = 5000
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
if (e.details?.type === "worktree.ready") {
|
||||
setBusy(e.name, false)
|
||||
WorktreeState.ready(e.name)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.details?.type === "worktree.failed") {
|
||||
setBusy(e.name, false)
|
||||
WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed"))
|
||||
return
|
||||
}
|
||||
|
||||
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
|
||||
const title =
|
||||
e.details.type === "permission.asked"
|
||||
@@ -551,6 +564,7 @@ export default function Layout(props: ParentProps) {
|
||||
const project = currentProject()
|
||||
if (!project) return
|
||||
|
||||
const local = project.worktree
|
||||
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
||||
const existing = store.workspaceOrder[project.worktree]
|
||||
if (!existing) {
|
||||
@@ -558,9 +572,9 @@ export default function Layout(props: ParentProps) {
|
||||
return
|
||||
}
|
||||
|
||||
const keep = existing.filter((d) => dirs.includes(d))
|
||||
const missing = dirs.filter((d) => !existing.includes(d))
|
||||
const merged = [...keep, ...missing]
|
||||
const keep = existing.filter((d) => d !== local && dirs.includes(d))
|
||||
const missing = dirs.filter((d) => d !== local && !existing.includes(d))
|
||||
const merged = [local, ...missing, ...keep]
|
||||
|
||||
if (merged.length !== existing.length) {
|
||||
setStore("workspaceOrder", project.worktree, merged)
|
||||
@@ -819,6 +833,49 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession(session: Session) {
|
||||
const [store, setStore] = globalSync.child(session.directory)
|
||||
const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
|
||||
const index = sessions.findIndex((s) => s.id === session.id)
|
||||
const nextSession = sessions[index + 1] ?? sessions[index - 1]
|
||||
|
||||
const result = await globalSDK.client.session
|
||||
.delete({ directory: session.directory, sessionID: session.id })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("session.delete.failed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return
|
||||
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const removed = new Set<string>([session.id])
|
||||
const collect = (parentID: string) => {
|
||||
for (const item of draft.session) {
|
||||
if (item.parentID !== parentID) continue
|
||||
removed.add(item.id)
|
||||
collect(item.id)
|
||||
}
|
||||
}
|
||||
collect(session.id)
|
||||
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
||||
}),
|
||||
)
|
||||
|
||||
if (session.id === params.id) {
|
||||
if (nextSession) {
|
||||
navigate(`/${params.dir}/session/${nextSession.id}`)
|
||||
} else {
|
||||
navigate(`/${params.dir}/session`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
command.register(() => {
|
||||
const commands: CommandOption[] = [
|
||||
{
|
||||
@@ -975,11 +1032,16 @@ export default function Layout(props: ParentProps) {
|
||||
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
|
||||
|
||||
async function renameProject(project: LocalProject, next: string) {
|
||||
if (!project.id) return
|
||||
const current = displayName(project)
|
||||
if (next === current) return
|
||||
const name = next === getFilename(project.worktree) ? "" : next
|
||||
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
|
||||
|
||||
if (project.id && project.id !== "global") {
|
||||
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
|
||||
return
|
||||
}
|
||||
|
||||
globalSync.project.meta(project.worktree, { name })
|
||||
}
|
||||
|
||||
async function renameSession(session: Session, next: string) {
|
||||
@@ -1125,16 +1187,53 @@ export default function Layout(props: ParentProps) {
|
||||
setBusy(directory, false)
|
||||
dismiss()
|
||||
|
||||
const href = `/${base64Encode(directory)}/session`
|
||||
navigate(href)
|
||||
layout.mobileSidebar.hide()
|
||||
|
||||
showToast({
|
||||
title: language.t("workspace.reset.success.title"),
|
||||
description: language.t("workspace.reset.success.description"),
|
||||
actions: [
|
||||
{
|
||||
label: language.t("command.session.new"),
|
||||
onClick: () => {
|
||||
const href = `/${base64Encode(directory)}/session`
|
||||
navigate(href)
|
||||
layout.mobileSidebar.hide()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: language.t("common.dismiss"),
|
||||
onClick: "dismiss",
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
function DialogDeleteSession(props: { session: Session }) {
|
||||
const handleDelete = async () => {
|
||||
await deleteSession(props.session)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={language.t("session.delete.title")} fit>
|
||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-14-regular text-text-strong">
|
||||
{language.t("session.delete.confirm", { name: props.session.title })}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||
{language.t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="large" onClick={handleDelete}>
|
||||
{language.t("session.delete.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDeleteWorkspace(props: { directory: string }) {
|
||||
const name = createMemo(() => getFilename(props.directory))
|
||||
const [data, setData] = createStore({
|
||||
@@ -1161,9 +1260,9 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
})
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteWorkspace(props.directory)
|
||||
const handleDelete = () => {
|
||||
dialog.close()
|
||||
void deleteWorkspace(props.directory)
|
||||
}
|
||||
|
||||
const description = () => {
|
||||
@@ -1349,17 +1448,22 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
function workspaceIds(project: LocalProject | undefined) {
|
||||
if (!project) return []
|
||||
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
||||
const local = project.worktree
|
||||
const dirs = [local, ...(project.sandboxes ?? [])]
|
||||
const active = currentProject()
|
||||
const directory = active?.worktree === project.worktree && params.dir ? base64Decode(params.dir) : undefined
|
||||
const next = directory && directory !== project.worktree && !dirs.includes(directory) ? [...dirs, directory] : dirs
|
||||
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
|
||||
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
||||
|
||||
const existing = store.workspaceOrder[project.worktree]
|
||||
if (!existing) return next
|
||||
if (!existing) return extra ? [...dirs, extra] : dirs
|
||||
|
||||
const keep = existing.filter((d) => next.includes(d))
|
||||
const missing = next.filter((d) => !existing.includes(d))
|
||||
return [...keep, ...missing]
|
||||
const keep = existing.filter((d) => d !== local && dirs.includes(d))
|
||||
const missing = dirs.filter((d) => d !== local && !existing.includes(d))
|
||||
const merged = [local, ...(pending && extra ? [extra] : []), ...missing, ...keep]
|
||||
if (!extra) return merged
|
||||
if (pending) return merged
|
||||
return [...merged, extra]
|
||||
}
|
||||
|
||||
function handleWorkspaceDragStart(event: unknown) {
|
||||
@@ -1475,6 +1579,8 @@ export default function Layout(props: ParentProps) {
|
||||
const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened())
|
||||
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
||||
const isActive = createMemo(() => props.session.id === params.id)
|
||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
||||
const [pendingRename, setPendingRename] = createSignal(false)
|
||||
|
||||
const messageLabel = (message: Message) => {
|
||||
const parts = sessionStore.part[message.id] ?? []
|
||||
@@ -1485,9 +1591,10 @@ export default function Layout(props: ParentProps) {
|
||||
const item = (
|
||||
<A
|
||||
href={`${props.slug}/session/${props.session.id}`}
|
||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menuOpen() ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
onMouseEnter={() => prefetchSession(props.session, "high")}
|
||||
onFocus={() => prefetchSession(props.session, "high")}
|
||||
onClick={() => setHoverSession(undefined)}
|
||||
>
|
||||
<div class="flex items-center gap-1 w-full">
|
||||
<div
|
||||
@@ -1555,46 +1662,115 @@ export default function Layout(props: ParentProps) {
|
||||
when={hoverReady()}
|
||||
fallback={<div class="text-12-regular text-text-weak">{language.t("session.messages.loading")}</div>}
|
||||
>
|
||||
<MessageNav
|
||||
messages={hoverMessages() ?? []}
|
||||
current={undefined}
|
||||
getLabel={messageLabel}
|
||||
onMessageSelect={(message) => {
|
||||
if (!isActive()) {
|
||||
sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`)
|
||||
navigate(`${props.slug}/session/${props.session.id}`)
|
||||
return
|
||||
}
|
||||
window.history.replaceState(null, "", `#message-${message.id}`)
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"))
|
||||
}}
|
||||
size="normal"
|
||||
class="w-60"
|
||||
/>
|
||||
<div class="overflow-y-auto max-h-72 h-full">
|
||||
<MessageNav
|
||||
messages={hoverMessages() ?? []}
|
||||
current={undefined}
|
||||
getLabel={messageLabel}
|
||||
onMessageSelect={(message) => {
|
||||
if (!isActive()) {
|
||||
sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`)
|
||||
navigate(`${props.slug}/session/${props.session.id}`)
|
||||
return
|
||||
}
|
||||
window.history.replaceState(null, "", `#message-${message.id}`)
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"))
|
||||
}}
|
||||
size="normal"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</HoverCard>
|
||||
</Show>
|
||||
<div
|
||||
class={`hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"}`}
|
||||
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
|
||||
classList={{
|
||||
"opacity-100 pointer-events-auto": menuOpen(),
|
||||
"opacity-0 pointer-events-none": !menuOpen(),
|
||||
"group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
||||
"group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
||||
}}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement={props.mobile ? "bottom" : "right"}
|
||||
title={language.t("command.session.archive")}
|
||||
keybind={command.keybind("session.archive")}
|
||||
gutter={8}
|
||||
>
|
||||
<IconButton
|
||||
icon="archive"
|
||||
variant="ghost"
|
||||
onClick={() => archiveSession(props.session)}
|
||||
aria-label={language.t("command.session.archive")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
|
||||
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (!pendingRename()) return
|
||||
event.preventDefault()
|
||||
setPendingRename(false)
|
||||
openEditor(`session:${props.session.id}`, props.session.title)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setPendingRename(true)
|
||||
setMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => archiveSession(props.session)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogDeleteSession session={props.session} />)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NewSessionItem = (props: { slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => {
|
||||
const label = language.t("command.session.new")
|
||||
const tooltip = () => props.mobile || !layout.sidebar.opened()
|
||||
const item = (
|
||||
<A
|
||||
href={`${props.slug}/session`}
|
||||
end
|
||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
onClick={() => setHoverSession(undefined)}
|
||||
>
|
||||
<div class="flex items-center gap-1 w-full">
|
||||
<div class="shrink-0 size-6 flex items-center justify-center">
|
||||
<Icon name="plus-small" size="small" class="text-icon-weak" />
|
||||
</div>
|
||||
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</A>
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3 hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active">
|
||||
<Show
|
||||
when={!tooltip()}
|
||||
fallback={
|
||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value={label} gutter={10}>
|
||||
{item}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SessionSkeleton = (props: { count?: number }): JSX.Element => {
|
||||
const items = Array.from({ length: props.count ?? 4 }, (_, index) => index)
|
||||
return (
|
||||
@@ -1781,9 +1957,6 @@ export default function Layout(props: ParentProps) {
|
||||
openEditor(`workspace:${props.directory}`, workspaceValue())
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Item onSelect={() => navigate(`/${slug()}/session`)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("command.session.new")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
disabled={local()}
|
||||
onSelect={() => {
|
||||
@@ -1808,19 +1981,6 @@ export default function Layout(props: ParentProps) {
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
<TooltipKeybind
|
||||
placement="right"
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
>
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md"
|
||||
onClick={() => navigate(`/${slug()}/session`)}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1828,16 +1988,9 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
<Collapsible.Content>
|
||||
<nav class="flex flex-col gap-1 px-2">
|
||||
<Button
|
||||
as={A}
|
||||
href={`${slug()}/session`}
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="edit"
|
||||
class="hidden _flex w-full text-left justify-start text-text-base rounded-md px-3"
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</Button>
|
||||
<Show when={workspaceSetting()}>
|
||||
<NewSessionItem slug={slug()} mobile={props.mobile} />
|
||||
</Show>
|
||||
<Show when={loading()}>
|
||||
<SessionSkeleton />
|
||||
</Show>
|
||||
@@ -1916,6 +2069,7 @@ export default function Layout(props: ParentProps) {
|
||||
"bg-surface-base-hover border border-border-weak-base": !selected() && open(),
|
||||
}}
|
||||
onClick={() => navigateToProject(props.project.worktree)}
|
||||
onBlur={() => setOpen(false)}
|
||||
>
|
||||
<ProjectIcon project={props.project} notify />
|
||||
</button>
|
||||
@@ -1937,7 +2091,22 @@ export default function Layout(props: ParentProps) {
|
||||
}}
|
||||
>
|
||||
<div class="-m-3 p-2 flex flex-col w-72">
|
||||
<div class="px-4 pt-2 pb-1 text-14-medium text-text-strong truncate">{displayName(props.project)}</div>
|
||||
<div class="px-4 pt-2 pb-1 flex items-center gap-2">
|
||||
<div class="text-14-medium text-text-strong truncate grow">{displayName(props.project)}</div>
|
||||
<Tooltip value={language.t("common.close")} placement="top" gutter={6}>
|
||||
<IconButton
|
||||
icon="circle-x"
|
||||
variant="ghost"
|
||||
class="shrink-0"
|
||||
aria-label={language.t("common.close")}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setOpen(false)
|
||||
closeProject(props.project.worktree)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="px-4 pb-2 text-12-medium text-text-weak">{language.t("sidebar.project.recentSessions")}</div>
|
||||
<div class="px-2 pb-2 flex flex-col gap-2">
|
||||
<Show
|
||||
@@ -1986,11 +2155,11 @@ export default function Layout(props: ParentProps) {
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent"
|
||||
onClick={() => {
|
||||
layout.sidebar.open()
|
||||
if (selected()) {
|
||||
setOpen(false)
|
||||
return
|
||||
}
|
||||
layout.sidebar.open()
|
||||
navigateToProject(props.project.worktree)
|
||||
}}
|
||||
>
|
||||
@@ -2028,6 +2197,9 @@ export default function Layout(props: ParentProps) {
|
||||
style={{ "overflow-anchor": "none" }}
|
||||
>
|
||||
<nav class="flex flex-col gap-1 px-2">
|
||||
<Show when={workspaceSetting()}>
|
||||
<NewSessionItem slug={slug()} mobile={props.mobile} />
|
||||
</Show>
|
||||
<Show when={loading()}>
|
||||
<SessionSkeleton />
|
||||
</Show>
|
||||
@@ -2084,8 +2256,19 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
if (!created?.directory) return
|
||||
|
||||
setBusy(created.directory, true)
|
||||
WorktreeState.pending(created.directory)
|
||||
setStore("workspaceExpanded", created.directory, true)
|
||||
setStore("workspaceOrder", current.worktree, (prev) => {
|
||||
const existing = prev ?? []
|
||||
const local = current.worktree
|
||||
const next = existing.filter((d) => d !== local && d !== created.directory)
|
||||
return [local, created.directory, ...next]
|
||||
})
|
||||
|
||||
globalSync.child(created.directory)
|
||||
navigate(`/${base64Encode(created.directory)}/session`)
|
||||
layout.mobileSidebar.hide()
|
||||
}
|
||||
|
||||
command.register(() => [
|
||||
@@ -2194,7 +2377,7 @@ export default function Layout(props: ParentProps) {
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
placement={sidebarProps.mobile ? "bottom" : "top"}
|
||||
placement="bottom"
|
||||
gutter={2}
|
||||
value={project()?.worktree}
|
||||
class="shrink-0"
|
||||
@@ -2343,7 +2526,8 @@ export default function Layout(props: ParentProps) {
|
||||
<div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
<Titlebar />
|
||||
<div class="flex-1 min-h-0 flex">
|
||||
<div
|
||||
<nav
|
||||
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
||||
classList={{
|
||||
"hidden xl:block": true,
|
||||
"relative shrink-0": true,
|
||||
@@ -2364,7 +2548,7 @@ export default function Layout(props: ParentProps) {
|
||||
onCollapse={layout.sidebar.close}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="xl:hidden">
|
||||
<div
|
||||
classList={{
|
||||
@@ -2376,7 +2560,8 @@ export default function Layout(props: ParentProps) {
|
||||
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
<nav
|
||||
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
||||
classList={{
|
||||
"@container fixed top-10 bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
|
||||
"translate-x-0": layout.mobileSidebar.opened(),
|
||||
@@ -2385,7 +2570,7 @@ export default function Layout(props: ParentProps) {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<SidebarContent mobile />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<main
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
58
packages/app/src/utils/worktree.ts
Normal file
58
packages/app/src/utils/worktree.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
const normalize = (directory: string) => directory.replace(/[\\/]+$/, "")
|
||||
|
||||
type State =
|
||||
| {
|
||||
status: "pending"
|
||||
}
|
||||
| {
|
||||
status: "ready"
|
||||
}
|
||||
| {
|
||||
status: "failed"
|
||||
message: string
|
||||
}
|
||||
|
||||
const state = new Map<string, State>()
|
||||
const waiters = new Map<string, Array<(state: State) => void>>()
|
||||
|
||||
export const Worktree = {
|
||||
get(directory: string) {
|
||||
return state.get(normalize(directory))
|
||||
},
|
||||
pending(directory: string) {
|
||||
const key = normalize(directory)
|
||||
const current = state.get(key)
|
||||
if (current && current.status !== "pending") return
|
||||
state.set(key, { status: "pending" })
|
||||
},
|
||||
ready(directory: string) {
|
||||
const key = normalize(directory)
|
||||
state.set(key, { status: "ready" })
|
||||
const list = waiters.get(key)
|
||||
if (!list) return
|
||||
waiters.delete(key)
|
||||
for (const fn of list) fn({ status: "ready" })
|
||||
},
|
||||
failed(directory: string, message: string) {
|
||||
const key = normalize(directory)
|
||||
state.set(key, { status: "failed", message })
|
||||
const list = waiters.get(key)
|
||||
if (!list) return
|
||||
waiters.delete(key)
|
||||
for (const fn of list) fn({ status: "failed", message })
|
||||
},
|
||||
wait(directory: string) {
|
||||
const key = normalize(directory)
|
||||
const current = state.get(key)
|
||||
if (current && current.status !== "pending") return Promise.resolve(current)
|
||||
|
||||
return new Promise<State>((resolve) => {
|
||||
const list = waiters.get(key)
|
||||
if (!list) {
|
||||
waiters.set(key, [resolve])
|
||||
return
|
||||
}
|
||||
list.push(resolve)
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "70K",
|
||||
full: "70,000",
|
||||
compact: "80K",
|
||||
full: "80,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ export const config = {
|
||||
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "500",
|
||||
commits: "7,000",
|
||||
monthlyUsers: "650,000",
|
||||
contributors: "600",
|
||||
commits: "7,500",
|
||||
monthlyUsers: "1.5M",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -219,8 +219,6 @@ function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
console.log(setupIntent)
|
||||
if (setupIntent?.status === "succeeded") {
|
||||
const pm = setupIntent.payment_method as PaymentMethod
|
||||
|
||||
|
||||
@@ -216,141 +216,71 @@ export async function POST(input: APIEvent) {
|
||||
})
|
||||
}
|
||||
if (body.type === "customer.subscription.created") {
|
||||
const data = {
|
||||
id: "evt_1Smq802SrMQ2Fneksse5FMNV",
|
||||
object: "event",
|
||||
api_version: "2025-07-30.basil",
|
||||
created: 1767766916,
|
||||
data: {
|
||||
object: {
|
||||
id: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
|
||||
object: "subscription",
|
||||
application: null,
|
||||
application_fee_percent: null,
|
||||
automatic_tax: {
|
||||
disabled_reason: null,
|
||||
enabled: false,
|
||||
liability: null,
|
||||
},
|
||||
billing_cycle_anchor: 1770445200,
|
||||
billing_cycle_anchor_config: null,
|
||||
billing_mode: {
|
||||
flexible: {
|
||||
proration_discounts: "included",
|
||||
},
|
||||
type: "flexible",
|
||||
updated_at: 1770445200,
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: "evt_1Smq802SrMQ2Fneksse5FMNV",
|
||||
object: "event",
|
||||
api_version: "2025-07-30.basil",
|
||||
created: 1767766916,
|
||||
data: {
|
||||
object: {
|
||||
id: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
|
||||
object: "subscription",
|
||||
application: null,
|
||||
application_fee_percent: null,
|
||||
automatic_tax: {
|
||||
disabled_reason: null,
|
||||
enabled: false,
|
||||
liability: null,
|
||||
},
|
||||
billing_cycle_anchor: 1770445200,
|
||||
billing_cycle_anchor_config: null,
|
||||
billing_mode: {
|
||||
flexible: {
|
||||
proration_discounts: "included",
|
||||
},
|
||||
type: "flexible",
|
||||
updated_at: 1770445200,
|
||||
},
|
||||
billing_thresholds: null,
|
||||
cancel_at: null,
|
||||
cancel_at_period_end: false,
|
||||
canceled_at: null,
|
||||
cancellation_details: {
|
||||
comment: null,
|
||||
feedback: null,
|
||||
reason: null,
|
||||
},
|
||||
collection_method: "charge_automatically",
|
||||
created: 1770445200,
|
||||
currency: "usd",
|
||||
customer: "cus_TkKmZZvysJ2wej",
|
||||
customer_account: null,
|
||||
days_until_due: null,
|
||||
default_payment_method: null,
|
||||
default_source: "card_1Smq7u2SrMQ2FneknjyOa7sq",
|
||||
default_tax_rates: [],
|
||||
description: null,
|
||||
discounts: [],
|
||||
ended_at: null,
|
||||
invoice_settings: {
|
||||
account_tax_ids: null,
|
||||
issuer: {
|
||||
type: "self",
|
||||
},
|
||||
},
|
||||
items: {
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: "si_TkKnBKXFX76t0O",
|
||||
object: "subscription_item",
|
||||
billing_thresholds: null,
|
||||
cancel_at: null,
|
||||
cancel_at_period_end: false,
|
||||
canceled_at: null,
|
||||
cancellation_details: {
|
||||
comment: null,
|
||||
feedback: null,
|
||||
reason: null,
|
||||
},
|
||||
collection_method: "charge_automatically",
|
||||
created: 1770445200,
|
||||
currency: "usd",
|
||||
customer: "cus_TkKmZZvysJ2wej",
|
||||
customer_account: null,
|
||||
days_until_due: null,
|
||||
default_payment_method: null,
|
||||
default_source: "card_1Smq7u2SrMQ2FneknjyOa7sq",
|
||||
default_tax_rates: [],
|
||||
description: null,
|
||||
current_period_end: 1772864400,
|
||||
current_period_start: 1770445200,
|
||||
discounts: [],
|
||||
ended_at: null,
|
||||
invoice_settings: {
|
||||
account_tax_ids: null,
|
||||
issuer: {
|
||||
type: "self",
|
||||
},
|
||||
},
|
||||
items: {
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: "si_TkKnBKXFX76t0O",
|
||||
object: "subscription_item",
|
||||
billing_thresholds: null,
|
||||
created: 1770445200,
|
||||
current_period_end: 1772864400,
|
||||
current_period_start: 1770445200,
|
||||
discounts: [],
|
||||
metadata: {},
|
||||
plan: {
|
||||
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
|
||||
object: "plan",
|
||||
active: true,
|
||||
amount: 20000,
|
||||
amount_decimal: "20000",
|
||||
billing_scheme: "per_unit",
|
||||
created: 1767725082,
|
||||
currency: "usd",
|
||||
interval: "month",
|
||||
interval_count: 1,
|
||||
livemode: false,
|
||||
metadata: {},
|
||||
meter: null,
|
||||
nickname: null,
|
||||
product: "prod_Tk9LjWT1n0DgYm",
|
||||
tiers_mode: null,
|
||||
transform_usage: null,
|
||||
trial_period_days: null,
|
||||
usage_type: "licensed",
|
||||
},
|
||||
price: {
|
||||
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
|
||||
object: "price",
|
||||
active: true,
|
||||
billing_scheme: "per_unit",
|
||||
created: 1767725082,
|
||||
currency: "usd",
|
||||
custom_unit_amount: null,
|
||||
livemode: false,
|
||||
lookup_key: null,
|
||||
metadata: {},
|
||||
nickname: null,
|
||||
product: "prod_Tk9LjWT1n0DgYm",
|
||||
recurring: {
|
||||
interval: "month",
|
||||
interval_count: 1,
|
||||
meter: null,
|
||||
trial_period_days: null,
|
||||
usage_type: "licensed",
|
||||
},
|
||||
tax_behavior: "unspecified",
|
||||
tiers_mode: null,
|
||||
transform_quantity: null,
|
||||
type: "recurring",
|
||||
unit_amount: 20000,
|
||||
unit_amount_decimal: "20000",
|
||||
},
|
||||
quantity: 1,
|
||||
subscription: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
|
||||
tax_rates: [],
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
total_count: 1,
|
||||
url: "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
|
||||
},
|
||||
latest_invoice: "in_1Smq7x2SrMQ2FnekSJesfPwE",
|
||||
livemode: false,
|
||||
metadata: {},
|
||||
next_pending_invoice_item_invoice: null,
|
||||
on_behalf_of: null,
|
||||
pause_collection: null,
|
||||
payment_settings: {
|
||||
payment_method_options: null,
|
||||
payment_method_types: null,
|
||||
save_default_payment_method: "off",
|
||||
},
|
||||
pending_invoice_item_interval: null,
|
||||
pending_setup_intent: null,
|
||||
pending_update: null,
|
||||
plan: {
|
||||
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
|
||||
object: "plan",
|
||||
@@ -372,29 +302,101 @@ export async function POST(input: APIEvent) {
|
||||
trial_period_days: null,
|
||||
usage_type: "licensed",
|
||||
},
|
||||
quantity: 1,
|
||||
schedule: null,
|
||||
start_date: 1770445200,
|
||||
status: "active",
|
||||
test_clock: "clock_1Smq6n2SrMQ2FnekQw4yt2PZ",
|
||||
transfer_data: null,
|
||||
trial_end: null,
|
||||
trial_settings: {
|
||||
end_behavior: {
|
||||
missing_payment_method: "create_invoice",
|
||||
price: {
|
||||
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
|
||||
object: "price",
|
||||
active: true,
|
||||
billing_scheme: "per_unit",
|
||||
created: 1767725082,
|
||||
currency: "usd",
|
||||
custom_unit_amount: null,
|
||||
livemode: false,
|
||||
lookup_key: null,
|
||||
metadata: {},
|
||||
nickname: null,
|
||||
product: "prod_Tk9LjWT1n0DgYm",
|
||||
recurring: {
|
||||
interval: "month",
|
||||
interval_count: 1,
|
||||
meter: null,
|
||||
trial_period_days: null,
|
||||
usage_type: "licensed",
|
||||
},
|
||||
tax_behavior: "unspecified",
|
||||
tiers_mode: null,
|
||||
transform_quantity: null,
|
||||
type: "recurring",
|
||||
unit_amount: 20000,
|
||||
unit_amount_decimal: "20000",
|
||||
},
|
||||
trial_start: null,
|
||||
quantity: 1,
|
||||
subscription: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
|
||||
tax_rates: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
total_count: 1,
|
||||
url: "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
|
||||
},
|
||||
latest_invoice: "in_1Smq7x2SrMQ2FnekSJesfPwE",
|
||||
livemode: false,
|
||||
metadata: {},
|
||||
next_pending_invoice_item_invoice: null,
|
||||
on_behalf_of: null,
|
||||
pause_collection: null,
|
||||
payment_settings: {
|
||||
payment_method_options: null,
|
||||
payment_method_types: null,
|
||||
save_default_payment_method: "off",
|
||||
},
|
||||
pending_invoice_item_interval: null,
|
||||
pending_setup_intent: null,
|
||||
pending_update: null,
|
||||
plan: {
|
||||
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
|
||||
object: "plan",
|
||||
active: true,
|
||||
amount: 20000,
|
||||
amount_decimal: "20000",
|
||||
billing_scheme: "per_unit",
|
||||
created: 1767725082,
|
||||
currency: "usd",
|
||||
interval: "month",
|
||||
interval_count: 1,
|
||||
livemode: false,
|
||||
pending_webhooks: 0,
|
||||
request: {
|
||||
id: "req_6YO9stvB155WJD",
|
||||
idempotency_key: "581ba059-6f86-49b2-9c49-0d8450255322",
|
||||
metadata: {},
|
||||
meter: null,
|
||||
nickname: null,
|
||||
product: "prod_Tk9LjWT1n0DgYm",
|
||||
tiers_mode: null,
|
||||
transform_usage: null,
|
||||
trial_period_days: null,
|
||||
usage_type: "licensed",
|
||||
},
|
||||
quantity: 1,
|
||||
schedule: null,
|
||||
start_date: 1770445200,
|
||||
status: "active",
|
||||
test_clock: "clock_1Smq6n2SrMQ2FnekQw4yt2PZ",
|
||||
transfer_data: null,
|
||||
trial_end: null,
|
||||
trial_settings: {
|
||||
end_behavior: {
|
||||
missing_payment_method: "create_invoice",
|
||||
},
|
||||
type: "customer.subscription.created",
|
||||
}
|
||||
},
|
||||
trial_start: null,
|
||||
},
|
||||
},
|
||||
livemode: false,
|
||||
pending_webhooks: 0,
|
||||
request: {
|
||||
id: "req_6YO9stvB155WJD",
|
||||
idempotency_key: "581ba059-6f86-49b2-9c49-0d8450255322",
|
||||
},
|
||||
type: "customer.subscription.created",
|
||||
}
|
||||
*/
|
||||
}
|
||||
if (body.type === "customer.subscription.deleted") {
|
||||
const subscriptionID = body.data.object.id
|
||||
@@ -419,7 +421,10 @@ export async function POST(input: APIEvent) {
|
||||
})
|
||||
}
|
||||
if (body.type === "invoice.payment_succeeded") {
|
||||
if (body.data.object.billing_reason === "subscription_cycle") {
|
||||
if (
|
||||
body.data.object.billing_reason === "subscription_cycle" ||
|
||||
body.data.object.billing_reason === "subscription_create"
|
||||
) {
|
||||
const invoiceID = body.data.object.id as string
|
||||
const amountInCents = body.data.object.amount_paid
|
||||
const customerID = body.data.object.customer as string
|
||||
|
||||
@@ -59,4 +59,84 @@
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
[data-slot="setting-row"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-4);
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="toggle-label"] {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 2.5rem;
|
||||
height: 1.5rem;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: #ccc;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.125rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + span {
|
||||
background-color: #21ad0e;
|
||||
border-color: #148605;
|
||||
|
||||
&::before {
|
||||
transform: translateX(1rem) translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover span {
|
||||
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
input:checked:hover + span {
|
||||
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
&:has(input:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input:disabled + span {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ import { action, useParams, useAction, useSubmission, json, query, createAsync }
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Show } from "solid-js"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import { Database, eq, and, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BillingTable, SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { Black } from "@opencode-ai/console-core/black.js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { queryBillingInfo } from "../../common"
|
||||
import styles from "./black-section.module.css"
|
||||
import waitlistStyles from "./black-waitlist-section.module.css"
|
||||
|
||||
const querySubscription = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
@@ -20,19 +21,25 @@ const querySubscription = query(async (workspaceID: string) => {
|
||||
fixedUsage: SubscriptionTable.fixedUsage,
|
||||
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
||||
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
||||
subscription: BillingTable.subscription,
|
||||
})
|
||||
.from(SubscriptionTable)
|
||||
.from(BillingTable)
|
||||
.innerJoin(SubscriptionTable, eq(SubscriptionTable.workspaceID, BillingTable.workspaceID))
|
||||
.where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted)))
|
||||
.then((r) => r[0]),
|
||||
)
|
||||
if (!row) return null
|
||||
if (!row?.subscription) return null
|
||||
|
||||
return {
|
||||
plan: row.subscription.plan,
|
||||
useBalance: row.subscription.useBalance ?? false,
|
||||
rollingUsage: Black.analyzeRollingUsage({
|
||||
plan: row.subscription.plan,
|
||||
usage: row.rollingUsage ?? 0,
|
||||
timeUpdated: row.timeRollingUpdated ?? new Date(),
|
||||
}),
|
||||
weeklyUsage: Black.analyzeWeeklyUsage({
|
||||
plan: row.subscription.plan,
|
||||
usage: row.fixedUsage ?? 0,
|
||||
timeUpdated: row.timeFixedUpdated ?? new Date(),
|
||||
}),
|
||||
@@ -53,6 +60,37 @@ function formatResetTime(seconds: number) {
|
||||
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`
|
||||
}
|
||||
|
||||
const cancelWaitlist = action(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
subscriptionPlan: null,
|
||||
timeSubscriptionBooked: null,
|
||||
timeSubscriptionSelected: null,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
)
|
||||
return { error: undefined }
|
||||
}, workspaceID).catch((e) => ({ error: e.message as string })),
|
||||
{ revalidate: [queryBillingInfo.key, querySubscription.key] },
|
||||
)
|
||||
}, "cancelWaitlist")
|
||||
|
||||
const enroll = action(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
await Billing.subscribe({ seats: 1 })
|
||||
return { error: undefined }
|
||||
}, workspaceID).catch((e) => ({ error: e.message as string })),
|
||||
{ revalidate: [queryBillingInfo.key, querySubscription.key] },
|
||||
)
|
||||
}, "enroll")
|
||||
|
||||
const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
|
||||
"use server"
|
||||
return json(
|
||||
@@ -66,17 +104,49 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
||||
})),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: queryBillingInfo.key },
|
||||
{ revalidate: [queryBillingInfo.key, querySubscription.key] },
|
||||
)
|
||||
}, "sessionUrl")
|
||||
|
||||
const setUseBalance = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
||||
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
subscription: useBalance
|
||||
? sql`JSON_SET(subscription, '$.useBalance', true)`
|
||||
: sql`JSON_REMOVE(subscription, '$.useBalance')`,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
)
|
||||
return { error: undefined }
|
||||
}, workspaceID).catch((e) => ({ error: e.message as string })),
|
||||
{ revalidate: [queryBillingInfo.key, querySubscription.key] },
|
||||
)
|
||||
}, "setUseBalance")
|
||||
|
||||
export function BlackSection() {
|
||||
const params = useParams()
|
||||
const billing = createAsync(() => queryBillingInfo(params.id!))
|
||||
const subscription = createAsync(() => querySubscription(params.id!))
|
||||
const sessionAction = useAction(createSessionUrl)
|
||||
const sessionSubmission = useSubmission(createSessionUrl)
|
||||
const subscription = createAsync(() => querySubscription(params.id!))
|
||||
const cancelAction = useAction(cancelWaitlist)
|
||||
const cancelSubmission = useSubmission(cancelWaitlist)
|
||||
const enrollAction = useAction(enroll)
|
||||
const enrollSubmission = useSubmission(enroll)
|
||||
const useBalanceSubmission = useSubmission(setUseBalance)
|
||||
const [store, setStore] = createStore({
|
||||
sessionRedirecting: false,
|
||||
cancelled: false,
|
||||
enrolled: false,
|
||||
})
|
||||
|
||||
async function onClickSession() {
|
||||
@@ -87,47 +157,113 @@ export function BlackSection() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onClickCancel() {
|
||||
const result = await cancelAction(params.id!)
|
||||
if (!result.error) {
|
||||
setStore("cancelled", true)
|
||||
}
|
||||
}
|
||||
|
||||
async function onClickEnroll() {
|
||||
const result = await enrollAction(params.id!)
|
||||
if (!result.error) {
|
||||
setStore("enrolled", true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Subscription</h2>
|
||||
<div data-slot="title-row">
|
||||
<p>You are subscribed to OpenCode Black for $200 per month.</p>
|
||||
<button
|
||||
data-color="primary"
|
||||
disabled={sessionSubmission.pending || store.sessionRedirecting}
|
||||
onClick={onClickSession}
|
||||
>
|
||||
{sessionSubmission.pending || store.sessionRedirecting ? "Loading..." : "Manage Subscription"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<Show when={subscription()}>
|
||||
{(sub) => (
|
||||
<div data-slot="usage">
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">5-hour Usage</span>
|
||||
<span data-slot="usage-value">{sub().rollingUsage.usagePercent}%</span>
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Subscription</h2>
|
||||
<div data-slot="title-row">
|
||||
<p>You are subscribed to OpenCode Black for ${sub().plan} per month.</p>
|
||||
<button
|
||||
data-color="primary"
|
||||
disabled={sessionSubmission.pending || store.sessionRedirecting}
|
||||
onClick={onClickSession}
|
||||
>
|
||||
{sessionSubmission.pending || store.sessionRedirecting ? "Loading..." : "Manage Subscription"}
|
||||
</button>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${sub().rollingUsage.usagePercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">Resets in {formatResetTime(sub().rollingUsage.resetInSec)}</span>
|
||||
</div>
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">Weekly Usage</span>
|
||||
<span data-slot="usage-value">{sub().weeklyUsage.usagePercent}%</span>
|
||||
<div data-slot="usage">
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">5-hour Usage</span>
|
||||
<span data-slot="usage-value">{sub().rollingUsage.usagePercent}%</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${sub().rollingUsage.usagePercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">Resets in {formatResetTime(sub().rollingUsage.resetInSec)}</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${sub().weeklyUsage.usagePercent}%` }} />
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">Weekly Usage</span>
|
||||
<span data-slot="usage-value">{sub().weeklyUsage.usagePercent}%</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${sub().weeklyUsage.usagePercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
|
||||
</div>
|
||||
<span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<form action={setUseBalance} method="post" data-slot="setting-row">
|
||||
<p>Use your available balance after reaching the usage limits</p>
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<input type="hidden" name="useBalance" value={sub().useBalance ? "false" : "true"} />
|
||||
<label data-slot="toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sub().useBalance}
|
||||
disabled={useBalanceSubmission.pending}
|
||||
onChange={(e) => e.currentTarget.form?.requestSubmit()}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</form>
|
||||
</section>
|
||||
)}
|
||||
</Show>
|
||||
</section>
|
||||
<Show when={billing()?.timeSubscriptionBooked}>
|
||||
<section class={waitlistStyles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Waitlist</h2>
|
||||
<div data-slot="title-row">
|
||||
<p>
|
||||
{billing()?.timeSubscriptionSelected
|
||||
? `We're ready to enroll you into the $${billing()?.subscriptionPlan} per month OpenCode Black plan.`
|
||||
: `You are on the waitlist for the $${billing()?.subscriptionPlan} per month OpenCode Black plan.`}
|
||||
</p>
|
||||
<button
|
||||
data-color="danger"
|
||||
disabled={cancelSubmission.pending || store.cancelled}
|
||||
onClick={onClickCancel}
|
||||
>
|
||||
{cancelSubmission.pending ? "Leaving..." : store.cancelled ? "Left" : "Leave Waitlist"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={billing()?.timeSubscriptionSelected}>
|
||||
<div data-slot="enroll-section">
|
||||
<button
|
||||
data-slot="enroll-button"
|
||||
data-color="primary"
|
||||
disabled={enrollSubmission.pending || store.enrolled}
|
||||
onClick={onClickEnroll}
|
||||
>
|
||||
{enrollSubmission.pending ? "Enrolling..." : store.enrolled ? "Enrolled" : "Enroll"}
|
||||
</button>
|
||||
<p data-slot="enroll-note">
|
||||
When you click Enroll, your subscription starts immediately and your card will be charged.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.root {
|
||||
[data-slot="title-row"] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
[data-slot="enroll-section"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
[data-slot="enroll-button"] {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
[data-slot="enroll-note"] {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export default function () {
|
||||
<div data-page="workspace-[id]">
|
||||
<div data-slot="sections">
|
||||
<Show when={sessionInfo()?.isAdmin}>
|
||||
<Show when={billingInfo()?.subscriptionID}>
|
||||
<Show when={billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked}>
|
||||
<BlackSection />
|
||||
</Show>
|
||||
<BillingSection />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Show, createMemo } from "solid-js"
|
||||
import { Match, Show, Switch, createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router"
|
||||
import { NewUserSection } from "./new-user-section"
|
||||
@@ -44,7 +44,7 @@ export default function () {
|
||||
<Show when={userInfo()?.isAdmin}>
|
||||
<span data-slot="billing-info">
|
||||
<Show
|
||||
when={billingInfo()?.reload}
|
||||
when={billingInfo()?.customerID}
|
||||
fallback={
|
||||
<button
|
||||
data-color="primary"
|
||||
|
||||
@@ -110,7 +110,11 @@ export const queryBillingInfo = query(async (workspaceID: string) => {
|
||||
timeMonthlyUsageUpdated: billing.timeMonthlyUsageUpdated,
|
||||
reloadError: billing.reloadError,
|
||||
timeReloadError: billing.timeReloadError,
|
||||
subscription: billing.subscription,
|
||||
subscriptionID: billing.subscriptionID,
|
||||
subscriptionPlan: billing.subscriptionPlan,
|
||||
timeSubscriptionBooked: billing.timeSubscriptionBooked,
|
||||
timeSubscriptionSelected: billing.timeSubscriptionSelected,
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "billing.get")
|
||||
|
||||
@@ -84,6 +84,7 @@ export async function handler(
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
const authInfo = await authenticate(modelInfo)
|
||||
const billingSource = validateBilling(authInfo, modelInfo)
|
||||
|
||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||
const providerInfo = selectProvider(
|
||||
@@ -96,7 +97,6 @@ export async function handler(
|
||||
retry,
|
||||
stickyProvider,
|
||||
)
|
||||
validateBilling(authInfo, modelInfo)
|
||||
validateModelSettings(authInfo)
|
||||
updateProviderKey(authInfo, providerInfo)
|
||||
logger.metric({ provider: providerInfo.id })
|
||||
@@ -183,7 +183,7 @@ export async function handler(
|
||||
const tokensInfo = providerInfo.normalizeUsage(json.usage)
|
||||
await trialLimiter?.track(tokensInfo)
|
||||
await rateLimiter?.track()
|
||||
const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
|
||||
const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, billingSource, tokensInfo)
|
||||
await reload(authInfo, costInfo)
|
||||
return new Response(body, {
|
||||
status: resStatus,
|
||||
@@ -219,7 +219,7 @@ export async function handler(
|
||||
if (usage) {
|
||||
const tokensInfo = providerInfo.normalizeUsage(usage)
|
||||
await trialLimiter?.track(tokensInfo)
|
||||
const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
|
||||
const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, billingSource, tokensInfo)
|
||||
await reload(authInfo, costInfo)
|
||||
}
|
||||
c.close()
|
||||
@@ -417,6 +417,7 @@ export async function handler(
|
||||
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
|
||||
reloadTrigger: BillingTable.reloadTrigger,
|
||||
timeReloadLockedTill: BillingTable.timeReloadLockedTill,
|
||||
subscription: BillingTable.subscription,
|
||||
},
|
||||
user: {
|
||||
id: UserTable.id,
|
||||
@@ -467,6 +468,7 @@ export async function handler(
|
||||
api_key: data.apiKey,
|
||||
workspace: data.workspaceID,
|
||||
isSubscription: data.subscription ? true : false,
|
||||
subscription: data.billing.subscription?.plan,
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -482,53 +484,58 @@ export async function handler(
|
||||
}
|
||||
|
||||
function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo) {
|
||||
if (!authInfo) return
|
||||
if (authInfo.provider?.credentials) return
|
||||
if (authInfo.isFree) return
|
||||
if (modelInfo.allowAnonymous) return
|
||||
if (!authInfo) return "anonymous"
|
||||
if (authInfo.provider?.credentials) return "free"
|
||||
if (authInfo.isFree) return "free"
|
||||
if (modelInfo.allowAnonymous) return "free"
|
||||
|
||||
// Validate subscription billing
|
||||
if (authInfo.subscription) {
|
||||
const black = BlackData.get()
|
||||
const sub = authInfo.subscription
|
||||
const now = new Date()
|
||||
if (authInfo.billing.subscription && authInfo.subscription) {
|
||||
try {
|
||||
const sub = authInfo.subscription
|
||||
const plan = authInfo.billing.subscription.plan
|
||||
|
||||
const formatRetryTime = (seconds: number) => {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
if (days >= 1) return `${days} day${days > 1 ? "s" : ""}`
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.ceil((seconds % 3600) / 60)
|
||||
if (hours >= 1) return `${hours}hr ${minutes}min`
|
||||
return `${minutes}min`
|
||||
const formatRetryTime = (seconds: number) => {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
if (days >= 1) return `${days} day${days > 1 ? "s" : ""}`
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.ceil((seconds % 3600) / 60)
|
||||
if (hours >= 1) return `${hours}hr ${minutes}min`
|
||||
return `${minutes}min`
|
||||
}
|
||||
|
||||
// Check weekly limit
|
||||
if (sub.fixedUsage && sub.timeFixedUpdated) {
|
||||
const result = Black.analyzeWeeklyUsage({
|
||||
plan,
|
||||
usage: sub.fixedUsage,
|
||||
timeUpdated: sub.timeFixedUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
|
||||
// Check rolling limit
|
||||
if (sub.rollingUsage && sub.timeRollingUpdated) {
|
||||
const result = Black.analyzeRollingUsage({
|
||||
plan,
|
||||
usage: sub.rollingUsage,
|
||||
timeUpdated: sub.timeRollingUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
|
||||
return "subscription"
|
||||
} catch (e) {
|
||||
if (!authInfo.billing.subscription.useBalance) throw e
|
||||
}
|
||||
|
||||
// Check weekly limit
|
||||
if (sub.fixedUsage && sub.timeFixedUpdated) {
|
||||
const result = Black.analyzeWeeklyUsage({
|
||||
usage: sub.fixedUsage,
|
||||
timeUpdated: sub.timeFixedUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
|
||||
// Check rolling limit
|
||||
if (sub.rollingUsage && sub.timeRollingUpdated) {
|
||||
const result = Black.analyzeRollingUsage({
|
||||
usage: sub.rollingUsage,
|
||||
timeUpdated: sub.timeRollingUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Validate pay as you go billing
|
||||
@@ -568,6 +575,8 @@ export async function handler(
|
||||
throw new UserLimitError(
|
||||
`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`,
|
||||
)
|
||||
|
||||
return "balance"
|
||||
}
|
||||
|
||||
function validateModelSettings(authInfo: AuthInfo) {
|
||||
@@ -584,6 +593,7 @@ export async function handler(
|
||||
authInfo: AuthInfo,
|
||||
modelInfo: ModelInfo,
|
||||
providerInfo: ProviderInfo,
|
||||
billingSource: ReturnType<typeof validateBilling>,
|
||||
usageInfo: UsageInfo,
|
||||
) {
|
||||
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
|
||||
@@ -640,7 +650,8 @@ export async function handler(
|
||||
"cost.total": Math.round(totalCostInCent),
|
||||
})
|
||||
|
||||
if (!authInfo) return
|
||||
if (billingSource === "anonymous") return
|
||||
authInfo = authInfo!
|
||||
|
||||
const cost = authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
|
||||
await Database.use((db) =>
|
||||
@@ -658,15 +669,16 @@ export async function handler(
|
||||
cacheWrite1hTokens,
|
||||
cost,
|
||||
keyID: authInfo.apiKeyId,
|
||||
enrichment: authInfo.subscription ? { plan: "sub" } : undefined,
|
||||
enrichment: billingSource === "subscription" ? { plan: "sub" } : undefined,
|
||||
}),
|
||||
db
|
||||
.update(KeyTable)
|
||||
.set({ timeUsed: sql`now()` })
|
||||
.where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
|
||||
...(authInfo.subscription
|
||||
...(billingSource === "subscription"
|
||||
? (() => {
|
||||
const black = BlackData.get()
|
||||
const plan = authInfo.billing.subscription!.plan
|
||||
const black = BlackData.getLimits({ plan })
|
||||
const week = getWeekBounds(new Date())
|
||||
const rollingWindowSeconds = black.rollingWindow * 3600
|
||||
return [
|
||||
|
||||
1
packages/console/core/migrations/0055_moaning_karnak.sql
Normal file
1
packages/console/core/migrations/0055_moaning_karnak.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `billing` ADD `time_subscription_selected` timestamp(3);
|
||||
1242
packages/console/core/migrations/meta/0055_snapshot.json
Normal file
1242
packages/console/core/migrations/meta/0055_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -386,6 +386,13 @@
|
||||
"when": 1768603665356,
|
||||
"tag": "0054_numerous_annihilus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 55,
|
||||
"version": "5",
|
||||
"when": 1769108945841,
|
||||
"tag": "0055_moaning_karnak",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -5,8 +5,11 @@ import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/bil
|
||||
import { Identifier } from "../src/identifier.js"
|
||||
import { centsToMicroCents } from "../src/util/price.js"
|
||||
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||
import { BlackData } from "../src/black.js"
|
||||
import { Actor } from "../src/actor.js"
|
||||
|
||||
const plan = "200"
|
||||
const couponID = "JAIr0Pe1"
|
||||
const workspaceID = process.argv[2]
|
||||
const seats = parseInt(process.argv[3])
|
||||
|
||||
@@ -61,16 +64,18 @@ const customerID =
|
||||
.then((customer) => customer.id))())
|
||||
console.log(`Customer ID: ${customerID}`)
|
||||
|
||||
const couponID = "JAIr0Pe1"
|
||||
const subscription = await Billing.stripe().subscriptions.create({
|
||||
customer: customerID!,
|
||||
items: [
|
||||
{
|
||||
price: `price_1SmfyI2StuRr0lbXovxJNeZn`,
|
||||
price: BlackData.planToPriceID({ plan }),
|
||||
discounts: [{ coupon: couponID }],
|
||||
quantity: seats,
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
workspaceID,
|
||||
},
|
||||
})
|
||||
console.log(`Subscription ID: ${subscription.id}`)
|
||||
|
||||
|
||||
40
packages/console/core/script/black-onboard-waitlist.ts
Normal file
40
packages/console/core/script/black-onboard-waitlist.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { subscribe } from "diagnostics_channel"
|
||||
import { Billing } from "../src/billing.js"
|
||||
import { and, Database, eq } from "../src/drizzle/index.js"
|
||||
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
|
||||
|
||||
const workspaceID = process.argv[2]
|
||||
|
||||
if (!workspaceID) {
|
||||
console.error("Usage: bun script/foo.ts <workspaceID>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Onboarding to Black waitlist`)
|
||||
|
||||
const billing = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
subscriptionPlan: BillingTable.subscriptionPlan,
|
||||
timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!billing?.timeSubscriptionBooked) {
|
||||
console.error(`Error: Workspace is not on the waitlist`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
timeSubscriptionSelected: new Date(),
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
)
|
||||
|
||||
console.log(`Done`)
|
||||
@@ -1,173 +0,0 @@
|
||||
import { Billing } from "../src/billing.js"
|
||||
import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js"
|
||||
import { UserTable } from "../src/schema/user.sql.js"
|
||||
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
|
||||
import { Identifier } from "../src/identifier.js"
|
||||
import { centsToMicroCents } from "../src/util/price.js"
|
||||
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||
|
||||
const workspaceID = process.argv[2]
|
||||
const email = process.argv[3]
|
||||
|
||||
console.log(`Onboarding workspace ${workspaceID} for email ${email}`)
|
||||
|
||||
if (!workspaceID || !email) {
|
||||
console.error("Usage: bun foo.ts <workspaceID> <email>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Look up the Stripe customer by email
|
||||
const customers = await Billing.stripe().customers.list({ email, limit: 10, expand: ["data.subscriptions"] })
|
||||
if (!customers.data) {
|
||||
console.error(`Error: No Stripe customer found for email ${email}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const customer = customers.data.find((c) => c.subscriptions?.data[0]?.items.data[0]?.price.unit_amount === 20000)
|
||||
if (!customer) {
|
||||
console.error(`Error: No Stripe customer found for email ${email} with $200 subscription`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const customerID = customer.id
|
||||
const subscription = customer.subscriptions!.data[0]
|
||||
const subscriptionID = subscription.id
|
||||
|
||||
// Validate the subscription is $200
|
||||
const amountInCents = subscription.items.data[0]?.price.unit_amount ?? 0
|
||||
if (amountInCents !== 20000) {
|
||||
console.error(`Error: Subscription amount is $${amountInCents / 100}, expected $200`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscription.id, { expand: ["discounts"] })
|
||||
const couponID =
|
||||
typeof subscriptionData.discounts[0] === "string"
|
||||
? subscriptionData.discounts[0]
|
||||
: subscriptionData.discounts[0]?.coupon?.id
|
||||
|
||||
// Check if subscription is already tied to another workspace
|
||||
const existingSubscription = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ workspaceID: BillingTable.workspaceID })
|
||||
.from(BillingTable)
|
||||
.where(sql`JSON_EXTRACT(${BillingTable.subscription}, '$.id') = ${subscriptionID}`)
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (existingSubscription) {
|
||||
console.error(
|
||||
`Error: Subscription ${subscriptionID} is already tied to workspace ${existingSubscription.workspaceID}`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Look up the workspace billing and check if it already has a customer id or subscription
|
||||
const billing = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ customerID: BillingTable.customerID, subscriptionID: BillingTable.subscriptionID })
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (billing?.subscriptionID) {
|
||||
console.error(`Error: Workspace ${workspaceID} already has a subscription: ${billing.subscriptionID}`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (billing?.customerID) {
|
||||
console.warn(
|
||||
`Warning: Workspace ${workspaceID} already has a customer id: ${billing.customerID}, replacing with ${customerID}`,
|
||||
)
|
||||
}
|
||||
|
||||
// Get the latest invoice and payment from the subscription
|
||||
const invoices = await Billing.stripe().invoices.list({
|
||||
subscription: subscriptionID,
|
||||
limit: 1,
|
||||
expand: ["data.payments"],
|
||||
})
|
||||
const invoice = invoices.data[0]
|
||||
const invoiceID = invoice?.id
|
||||
const paymentID = invoice?.payments?.data[0]?.payment.payment_intent as string | undefined
|
||||
|
||||
// Get the default payment method from the customer
|
||||
const paymentMethodID = (customer.invoice_settings.default_payment_method ?? subscription.default_payment_method) as
|
||||
| string
|
||||
| null
|
||||
const paymentMethod = paymentMethodID ? await Billing.stripe().paymentMethods.retrieve(paymentMethodID) : null
|
||||
const paymentMethodLast4 = paymentMethod?.card?.last4 ?? null
|
||||
const paymentMethodType = paymentMethod?.type ?? null
|
||||
|
||||
// Look up the user in the workspace
|
||||
const users = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ id: UserTable.id, email: AuthTable.subject })
|
||||
.from(UserTable)
|
||||
.innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
|
||||
.where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
|
||||
)
|
||||
if (users.length === 0) {
|
||||
console.error(`Error: No users found in workspace ${workspaceID}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const user = users.length === 1 ? users[0] : users.find((u) => u.email === email)
|
||||
if (!user) {
|
||||
console.error(`Error: User with email ${email} not found in workspace ${workspaceID}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Set workspaceID in Stripe customer metadata
|
||||
await Billing.stripe().customers.update(customerID, {
|
||||
metadata: {
|
||||
workspaceID,
|
||||
},
|
||||
})
|
||||
|
||||
await Database.transaction(async (tx) => {
|
||||
// Set customer id, subscription id, and payment method on workspace billing
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
customerID,
|
||||
subscriptionID,
|
||||
paymentMethodID,
|
||||
paymentMethodLast4,
|
||||
paymentMethodType,
|
||||
subscription: {
|
||||
status: "subscribed",
|
||||
coupon: couponID,
|
||||
seats: 1,
|
||||
plan: "200",
|
||||
},
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
|
||||
// Create a row in subscription table
|
||||
await tx.insert(SubscriptionTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("subscription"),
|
||||
userID: user.id,
|
||||
})
|
||||
|
||||
// Create a row in payments table
|
||||
await tx.insert(PaymentTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("payment"),
|
||||
amount: centsToMicroCents(amountInCents),
|
||||
customerID,
|
||||
invoiceID,
|
||||
paymentID,
|
||||
enrichment: {
|
||||
type: "subscription",
|
||||
couponID,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
console.log(`Successfully onboarded workspace ${workspaceID}`)
|
||||
console.log(` Customer ID: ${customerID}`)
|
||||
console.log(` Subscription ID: ${subscriptionID}`)
|
||||
console.log(
|
||||
` Payment Method: ${paymentMethodID ?? "(none)"} (${paymentMethodType ?? "unknown"} ending in ${paymentMethodLast4 ?? "????"})`,
|
||||
)
|
||||
console.log(` User ID: ${user.id}`)
|
||||
console.log(` Invoice ID: ${invoiceID ?? "(none)"}`)
|
||||
console.log(` Payment ID: ${paymentID ?? "(none)"}`)
|
||||
41
packages/console/core/script/black-select-workspaces.ts
Normal file
41
packages/console/core/script/black-select-workspaces.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Database, eq, and, sql, inArray, isNull, count } from "../src/drizzle/index.js"
|
||||
import { BillingTable, SubscriptionPlan } from "../src/schema/billing.sql.js"
|
||||
import { UserTable } from "../src/schema/user.sql.js"
|
||||
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||
|
||||
const plan = process.argv[2] as (typeof SubscriptionPlan)[number]
|
||||
if (!SubscriptionPlan.includes(plan)) {
|
||||
console.error("Usage: bun foo.ts <count>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const workspaces = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ workspaceID: BillingTable.workspaceID })
|
||||
.from(BillingTable)
|
||||
.where(and(eq(BillingTable.subscriptionPlan, plan), isNull(BillingTable.timeSubscriptionSelected)))
|
||||
.orderBy(sql`RAND()`)
|
||||
.limit(100),
|
||||
)
|
||||
|
||||
console.log(`Found ${workspaces.length} workspaces on Black ${plan} waitlist`)
|
||||
|
||||
console.log("== Workspace IDs ==")
|
||||
const ids = workspaces.map((w) => w.workspaceID)
|
||||
for (const id of ids) {
|
||||
console.log(id)
|
||||
}
|
||||
|
||||
console.log("\n== User Emails ==")
|
||||
const emails = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ email: AuthTable.subject })
|
||||
.from(UserTable)
|
||||
.innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
|
||||
.where(inArray(UserTable.workspaceID, ids)),
|
||||
)
|
||||
|
||||
const unique = new Set(emails.map((row) => row.email))
|
||||
for (const email of unique) {
|
||||
console.log(email)
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Database, and, eq, sql } from "../src/drizzle/index.js"
|
||||
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||
import { UserTable } from "../src/schema/user.sql.js"
|
||||
import { BillingTable, PaymentTable, SubscriptionTable, UsageTable } from "../src/schema/billing.sql.js"
|
||||
import {
|
||||
BillingTable,
|
||||
PaymentTable,
|
||||
SubscriptionTable,
|
||||
SubscriptionPlan,
|
||||
UsageTable,
|
||||
} from "../src/schema/billing.sql.js"
|
||||
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
|
||||
import { BlackData } from "../src/black.js"
|
||||
import { centsToMicroCents } from "../src/util/price.js"
|
||||
@@ -86,8 +92,10 @@ async function printWorkspace(workspaceID: string) {
|
||||
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
||||
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
||||
timeSubscriptionCreated: SubscriptionTable.timeCreated,
|
||||
subscription: BillingTable.subscription,
|
||||
})
|
||||
.from(UserTable)
|
||||
.innerJoin(BillingTable, eq(BillingTable.workspaceID, workspace.id))
|
||||
.leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
|
||||
.leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
|
||||
.where(eq(UserTable.workspaceID, workspace.id))
|
||||
@@ -121,14 +129,17 @@ async function printWorkspace(workspaceID: string) {
|
||||
booked: BillingTable.timeSubscriptionBooked,
|
||||
enrichment: BillingTable.subscription,
|
||||
},
|
||||
timeSubscriptionSelected: BillingTable.timeSubscriptionSelected,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, workspace.id))
|
||||
.then(
|
||||
(rows) =>
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
balance: `$${(row.balance / 100000000).toFixed(2)}`,
|
||||
reload: row.reload ? "yes" : "no",
|
||||
customerID: row.customerID,
|
||||
subscriptionID: row.subscriptionID,
|
||||
subscription: row.subscriptionID
|
||||
? [
|
||||
`Black ${row.subscription.enrichment!.plan}`,
|
||||
@@ -137,7 +148,7 @@ async function printWorkspace(workspaceID: string) {
|
||||
`(ref: ${row.subscriptionID})`,
|
||||
].join(" ")
|
||||
: row.subscription.booked
|
||||
? `Waitlist ${row.subscription.plan} plan`
|
||||
? `Waitlist ${row.subscription.plan} plan${row.timeSubscriptionSelected ? " (selected)" : ""}`
|
||||
: undefined,
|
||||
}))[0],
|
||||
),
|
||||
@@ -223,17 +234,20 @@ function formatRetryTime(seconds: number) {
|
||||
}
|
||||
|
||||
function getSubscriptionStatus(row: {
|
||||
subscription: {
|
||||
plan: (typeof SubscriptionPlan)[number]
|
||||
} | null
|
||||
timeSubscriptionCreated: Date | null
|
||||
fixedUsage: number | null
|
||||
rollingUsage: number | null
|
||||
timeFixedUpdated: Date | null
|
||||
timeRollingUpdated: Date | null
|
||||
}) {
|
||||
if (!row.timeSubscriptionCreated) {
|
||||
if (!row.timeSubscriptionCreated || !row.subscription) {
|
||||
return { weekly: null, rolling: null, rateLimited: null, retryIn: null }
|
||||
}
|
||||
|
||||
const black = BlackData.get()
|
||||
const black = BlackData.getLimits({ plan: row.subscription.plan })
|
||||
const now = new Date()
|
||||
const week = getWeekBounds(now)
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
// read the secret
|
||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||
const lines = ret.split("\n")
|
||||
const value = lines.find((line) => line.startsWith("ZEN_BLACK"))?.split("=")[1]
|
||||
if (!value) throw new Error("ZEN_BLACK not found")
|
||||
const value = lines.find((line) => line.startsWith("ZEN_BLACK_LIMITS"))?.split("=")[1]
|
||||
if (!value) throw new Error("ZEN_BLACK_LIMITS not found")
|
||||
|
||||
// validate value
|
||||
BlackData.validate(JSON.parse(value))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_BLACK ${value} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_BLACK_LIMITS ${value} --stage ${stage}`
|
||||
|
||||
@@ -8,10 +8,10 @@ import { BlackData } from "../src/black"
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const secrets = await $`bun sst secret list`.cwd(root).text()
|
||||
|
||||
// read the line starting with "ZEN_BLACK"
|
||||
// read value
|
||||
const lines = secrets.split("\n")
|
||||
const oldValue = lines.find((line) => line.startsWith("ZEN_BLACK"))?.split("=")[1]
|
||||
if (!oldValue) throw new Error("ZEN_BLACK not found")
|
||||
const oldValue = lines.find((line) => line.startsWith("ZEN_BLACK_LIMITS"))?.split("=")[1] ?? "{}"
|
||||
if (!oldValue) throw new Error("ZEN_BLACK_LIMITS not found")
|
||||
|
||||
// store the prettified json to a temp file
|
||||
const filename = `black-${Date.now()}.json`
|
||||
@@ -25,4 +25,4 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
||||
BlackData.validate(JSON.parse(newValue))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_BLACK ${newValue}`
|
||||
await $`bun sst secret set ZEN_BLACK_LIMITS ${newValue}`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Stripe } from "stripe"
|
||||
import { Database, eq, sql } from "./drizzle"
|
||||
import { BillingTable, PaymentTable, UsageTable } from "./schema/billing.sql"
|
||||
import { BillingTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql"
|
||||
import { Actor } from "./actor"
|
||||
import { fn } from "./util/fn"
|
||||
import { z } from "zod"
|
||||
@@ -8,6 +8,7 @@ import { Resource } from "@opencode-ai/console-resource"
|
||||
import { Identifier } from "./identifier"
|
||||
import { centsToMicroCents } from "./util/price"
|
||||
import { User } from "./user"
|
||||
import { BlackData } from "./black"
|
||||
|
||||
export namespace Billing {
|
||||
export const ITEM_CREDIT_NAME = "opencode credits"
|
||||
@@ -288,4 +289,69 @@ export namespace Billing {
|
||||
return charge.receipt_url
|
||||
},
|
||||
)
|
||||
|
||||
export const subscribe = fn(
|
||||
z.object({
|
||||
seats: z.number(),
|
||||
coupon: z.string().optional(),
|
||||
}),
|
||||
async ({ seats, coupon }) => {
|
||||
const user = Actor.assert("user")
|
||||
const billing = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
customerID: BillingTable.customerID,
|
||||
paymentMethodID: BillingTable.paymentMethodID,
|
||||
subscriptionID: BillingTable.subscriptionID,
|
||||
subscriptionPlan: BillingTable.subscriptionPlan,
|
||||
timeSubscriptionSelected: BillingTable.timeSubscriptionSelected,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, Actor.workspace()))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!billing) throw new Error("Billing record not found")
|
||||
if (!billing.timeSubscriptionSelected) throw new Error("Not selected for subscription")
|
||||
if (billing.subscriptionID) throw new Error("Already subscribed")
|
||||
if (!billing.customerID) throw new Error("No customer ID")
|
||||
if (!billing.paymentMethodID) throw new Error("No payment method")
|
||||
if (!billing.subscriptionPlan) throw new Error("No subscription plan")
|
||||
|
||||
const subscription = await Billing.stripe().subscriptions.create({
|
||||
customer: billing.customerID,
|
||||
default_payment_method: billing.paymentMethodID,
|
||||
items: [{ price: BlackData.planToPriceID({ plan: billing.subscriptionPlan }) }],
|
||||
metadata: {
|
||||
workspaceID: Actor.workspace(),
|
||||
},
|
||||
})
|
||||
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
subscriptionID: subscription.id,
|
||||
subscription: {
|
||||
status: "subscribed",
|
||||
coupon,
|
||||
seats,
|
||||
plan: billing.subscriptionPlan!,
|
||||
},
|
||||
subscriptionPlan: null,
|
||||
timeSubscriptionBooked: null,
|
||||
timeSubscriptionSelected: null,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, Actor.workspace()))
|
||||
|
||||
await tx.insert(SubscriptionTable).values({
|
||||
workspaceID: Actor.workspace(),
|
||||
id: Identifier.create("subscription"),
|
||||
userID: user.properties.userID,
|
||||
})
|
||||
})
|
||||
|
||||
return subscription.id
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,33 +3,74 @@ import { fn } from "./util/fn"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { centsToMicroCents } from "./util/price"
|
||||
import { getWeekBounds } from "./util/date"
|
||||
import { SubscriptionPlan } from "./schema/billing.sql"
|
||||
|
||||
export namespace BlackData {
|
||||
const Schema = z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
"200": z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
}),
|
||||
"100": z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
}),
|
||||
"20": z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const validate = fn(Schema, (input) => {
|
||||
return input
|
||||
})
|
||||
|
||||
export const get = fn(z.void(), () => {
|
||||
const json = JSON.parse(Resource.ZEN_BLACK.value)
|
||||
return Schema.parse(json)
|
||||
})
|
||||
export const getLimits = fn(
|
||||
z.object({
|
||||
plan: z.enum(SubscriptionPlan),
|
||||
}),
|
||||
({ plan }) => {
|
||||
const json = JSON.parse(Resource.ZEN_BLACK_LIMITS.value)
|
||||
return Schema.parse(json)[plan]
|
||||
},
|
||||
)
|
||||
|
||||
export const planToPriceID = fn(
|
||||
z.object({
|
||||
plan: z.enum(SubscriptionPlan),
|
||||
}),
|
||||
({ plan }) => {
|
||||
if (plan === "200") return Resource.ZEN_BLACK_PRICE.plan200
|
||||
if (plan === "100") return Resource.ZEN_BLACK_PRICE.plan100
|
||||
return Resource.ZEN_BLACK_PRICE.plan20
|
||||
},
|
||||
)
|
||||
|
||||
export const priceIDToPlan = fn(
|
||||
z.object({
|
||||
priceID: z.string(),
|
||||
}),
|
||||
({ priceID }) => {
|
||||
if (priceID === Resource.ZEN_BLACK_PRICE.plan200) return "200"
|
||||
if (priceID === Resource.ZEN_BLACK_PRICE.plan100) return "100"
|
||||
return "20"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export namespace Black {
|
||||
export const analyzeRollingUsage = fn(
|
||||
z.object({
|
||||
plan: z.enum(SubscriptionPlan),
|
||||
usage: z.number().int(),
|
||||
timeUpdated: z.date(),
|
||||
}),
|
||||
({ usage, timeUpdated }) => {
|
||||
({ plan, usage, timeUpdated }) => {
|
||||
const now = new Date()
|
||||
const black = BlackData.get()
|
||||
const black = BlackData.getLimits({ plan })
|
||||
const rollingWindowMs = black.rollingWindow * 3600 * 1000
|
||||
const rollingLimitInMicroCents = centsToMicroCents(black.rollingLimit * 100)
|
||||
const windowStart = new Date(now.getTime() - rollingWindowMs)
|
||||
@@ -59,11 +100,12 @@ export namespace Black {
|
||||
|
||||
export const analyzeWeeklyUsage = fn(
|
||||
z.object({
|
||||
plan: z.enum(SubscriptionPlan),
|
||||
usage: z.number().int(),
|
||||
timeUpdated: z.date(),
|
||||
}),
|
||||
({ usage, timeUpdated }) => {
|
||||
const black = BlackData.get()
|
||||
({ plan, usage, timeUpdated }) => {
|
||||
const black = BlackData.getLimits({ plan })
|
||||
const now = new Date()
|
||||
const week = getWeekBounds(now)
|
||||
const fixedLimitInMicroCents = centsToMicroCents(black.fixedLimit * 100)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex,
|
||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
export const SubscriptionPlan = ["20", "100", "200"] as const
|
||||
export const BillingTable = mysqlTable(
|
||||
"billing",
|
||||
{
|
||||
@@ -23,13 +24,15 @@ export const BillingTable = mysqlTable(
|
||||
timeReloadLockedTill: utc("time_reload_locked_till"),
|
||||
subscription: json("subscription").$type<{
|
||||
status: "subscribed"
|
||||
coupon?: string
|
||||
seats: number
|
||||
plan: "20" | "100" | "200"
|
||||
useBalance?: boolean
|
||||
coupon?: string
|
||||
}>(),
|
||||
subscriptionID: varchar("subscription_id", { length: 28 }),
|
||||
subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
|
||||
subscriptionPlan: mysqlEnum("subscription_plan", SubscriptionPlan),
|
||||
timeSubscriptionBooked: utc("time_subscription_booked"),
|
||||
timeSubscriptionSelected: utc("time_subscription_selected"),
|
||||
},
|
||||
(table) => [
|
||||
...workspaceIndexes(table),
|
||||
|
||||
9
packages/console/core/sst-env.d.ts
vendored
9
packages/console/core/sst-env.d.ts
vendored
@@ -118,10 +118,17 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_BLACK": {
|
||||
"ZEN_BLACK_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_BLACK_PRICE": {
|
||||
"plan100": string
|
||||
"plan20": string
|
||||
"plan200": string
|
||||
"product": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
9
packages/console/function/sst-env.d.ts
vendored
9
packages/console/function/sst-env.d.ts
vendored
@@ -118,10 +118,17 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_BLACK": {
|
||||
"ZEN_BLACK_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_BLACK_PRICE": {
|
||||
"plan100": string
|
||||
"plan20": string
|
||||
"plan200": string
|
||||
"product": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
9
packages/console/resource/sst-env.d.ts
vendored
9
packages/console/resource/sst-env.d.ts
vendored
@@ -118,10 +118,17 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_BLACK": {
|
||||
"ZEN_BLACK_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_BLACK_PRICE": {
|
||||
"plan100": string
|
||||
"plan20": string
|
||||
"plan200": string
|
||||
"product": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.1.31",
|
||||
"version": "1.1.34",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
99
packages/desktop/src-tauri/Cargo.lock
generated
99
packages/desktop/src-tauri/Cargo.lock
generated
@@ -464,6 +464,15 @@ dependencies = [
|
||||
"toml 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "caseless"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8"
|
||||
dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.47"
|
||||
@@ -574,6 +583,23 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "comrak"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "321d20bf105b6871a49da44c5fbb93e90a7cd6178ea5a9fe6cbc1e6d4504bc5e"
|
||||
dependencies = [
|
||||
"caseless",
|
||||
"entities",
|
||||
"jetscii",
|
||||
"phf 0.13.1",
|
||||
"phf_codegen 0.13.1",
|
||||
"rustc-hash",
|
||||
"smallvec",
|
||||
"typed-arena",
|
||||
"unicode_categories",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -1053,6 +1079,12 @@ dependencies = [
|
||||
"windows 0.51.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "entities"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
@@ -2153,6 +2185,12 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jetscii"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -2986,6 +3024,7 @@ dependencies = [
|
||||
name = "opencode-desktop"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"comrak",
|
||||
"futures",
|
||||
"gtk",
|
||||
"listeners",
|
||||
@@ -3187,6 +3226,16 @@ dependencies = [
|
||||
"phf_shared 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_shared 0.13.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.8.0"
|
||||
@@ -3207,6 +3256,16 @@ dependencies = [
|
||||
"phf_shared 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
|
||||
dependencies = [
|
||||
"phf_generator 0.13.1",
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.8.0"
|
||||
@@ -3237,6 +3296,16 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.10.0"
|
||||
@@ -3291,6 +3360,15 @@ dependencies = [
|
||||
"siphasher 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||
dependencies = [
|
||||
"siphasher 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@@ -5478,6 +5556,12 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typed-arena"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
@@ -5548,12 +5632,27 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_categories"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
||||
@@ -41,6 +41,7 @@ semver = "1.0.27"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
tauri-plugin-decorum = "1.1.1"
|
||||
comrak = { version = "0.50", default-features = false }
|
||||
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod cli;
|
||||
#[cfg(windows)]
|
||||
mod job_object;
|
||||
mod markdown;
|
||||
mod window_customizer;
|
||||
|
||||
use cli::{install_cli, sync_cli};
|
||||
@@ -151,17 +152,20 @@ fn get_sidecar_port() -> u32 {
|
||||
}) as u32
|
||||
}
|
||||
|
||||
fn spawn_sidecar(app: &AppHandle, port: u32, password: &str) -> CommandChild {
|
||||
fn spawn_sidecar(app: &AppHandle, hostname: &str, port: u32, password: &str) -> CommandChild {
|
||||
let log_state = app.state::<LogState>();
|
||||
let log_state_clone = log_state.inner().clone();
|
||||
|
||||
println!("spawning sidecar on port {port}");
|
||||
|
||||
let (mut rx, child) = cli::create_command(app, format!("serve --port {port}").as_str())
|
||||
.env("OPENCODE_SERVER_USERNAME", "opencode")
|
||||
.env("OPENCODE_SERVER_PASSWORD", password)
|
||||
.spawn()
|
||||
.expect("Failed to spawn opencode");
|
||||
let (mut rx, child) = cli::create_command(
|
||||
app,
|
||||
format!("serve --hostname {hostname} --port {port}").as_str(),
|
||||
)
|
||||
.env("OPENCODE_SERVER_USERNAME", "opencode")
|
||||
.env("OPENCODE_SERVER_PASSWORD", password)
|
||||
.spawn()
|
||||
.expect("Failed to spawn opencode");
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
@@ -283,7 +287,8 @@ pub fn run() {
|
||||
install_cli,
|
||||
ensure_server_ready,
|
||||
get_default_server_url,
|
||||
set_default_server_url
|
||||
set_default_server_url,
|
||||
markdown::parse_markdown_command
|
||||
])
|
||||
.setup(move |app| {
|
||||
let app = app.handle().clone();
|
||||
@@ -398,17 +403,37 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Converts a bind address hostname to a valid URL hostname for connection.
|
||||
/// - `0.0.0.0` and `::` are wildcard bind addresses, not valid connect targets
|
||||
/// - IPv6 addresses need brackets in URLs (e.g., `::1` -> `[::1]`)
|
||||
fn normalize_hostname_for_url(hostname: &str) -> String {
|
||||
// Wildcard bind addresses -> localhost equivalents
|
||||
if hostname == "0.0.0.0" {
|
||||
return "127.0.0.1".to_string();
|
||||
}
|
||||
if hostname == "::" {
|
||||
return "[::1]".to_string();
|
||||
}
|
||||
|
||||
// IPv6 addresses need brackets in URLs
|
||||
if hostname.contains(':') && !hostname.starts_with('[') {
|
||||
return format!("[{}]", hostname);
|
||||
}
|
||||
|
||||
hostname.to_string()
|
||||
}
|
||||
|
||||
fn get_server_url_from_config(config: &cli::Config) -> Option<String> {
|
||||
let server = config.server.as_ref()?;
|
||||
let port = server.port?;
|
||||
println!("server.port found in OC config: {port}");
|
||||
let hostname = server.hostname.as_ref();
|
||||
let hostname = server
|
||||
.hostname
|
||||
.as_ref()
|
||||
.map(|v| normalize_hostname_for_url(v))
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||
|
||||
Some(format!(
|
||||
"http://{}:{}",
|
||||
hostname.map(|v| v.as_str()).unwrap_or("127.0.0.1"),
|
||||
port
|
||||
))
|
||||
Some(format!("http://{}:{}", hostname, port))
|
||||
}
|
||||
|
||||
async fn setup_server_connection(
|
||||
@@ -448,12 +473,13 @@ async fn setup_server_connection(
|
||||
}
|
||||
|
||||
let local_port = get_sidecar_port();
|
||||
let local_url = format!("http://127.0.0.1:{local_port}");
|
||||
let hostname = "127.0.0.1";
|
||||
let local_url = format!("http://{hostname}:{local_port}");
|
||||
|
||||
if !check_server_health(&local_url, None).await {
|
||||
let password = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
match spawn_local_server(app, local_port, &password).await {
|
||||
match spawn_local_server(app, hostname, local_port, &password).await {
|
||||
Ok(child) => Ok((
|
||||
Some(child),
|
||||
ServerReadyData {
|
||||
@@ -476,11 +502,12 @@ async fn setup_server_connection(
|
||||
|
||||
async fn spawn_local_server(
|
||||
app: &AppHandle,
|
||||
hostname: &str,
|
||||
port: u32,
|
||||
password: &str,
|
||||
) -> Result<CommandChild, String> {
|
||||
let child = spawn_sidecar(app, port, password);
|
||||
let url = format!("http://127.0.0.1:{port}");
|
||||
let child = spawn_sidecar(app, hostname, port, password);
|
||||
let url = format!("http://{hostname}:{port}");
|
||||
|
||||
let timestamp = Instant::now();
|
||||
loop {
|
||||
|
||||
@@ -52,6 +52,8 @@ fn configure_display_backend() -> Option<String> {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
unsafe { std::env::set_var("NO_PROXY", "127.0.0.1,localhost,::1") };
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(backend_note) = configure_display_backend() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user