Compare commits

..

124 Commits

Author SHA1 Message Date
Dax Raad
1de66812bf Merge branch 'dev' into sqlite2 2026-02-09 17:53:05 -05:00
Surma
832902c8e3 fix: publish session.error event for invalid model selection (#8451) 2026-02-09 16:27:48 -06:00
Luke Parker
3d6fb29f0c fix(desktop): correct module name for linux_display in main.rs (#12862) 2026-02-09 21:13:47 +00:00
Adam
9824370f82 chore: more defensive 2026-02-09 14:12:23 -06:00
Adam
371e106faa chore: cleanup 2026-02-09 14:02:14 -06:00
Adam
19809e7680 fix(app): max widths 2026-02-09 13:59:26 -06:00
opencode-agent[bot]
389afef336 chore: generate 2026-02-09 19:57:32 +00:00
Adam
274bb948e7 fix(docs): locale markdown issues 2026-02-09 13:55:55 -06:00
opencode-agent[bot]
d9b4535d64 chore: generate 2026-02-09 19:27:52 +00:00
Adam
3dc720ff9c fix: locale routing 2026-02-09 13:26:50 -06:00
Bryce Ryan
56b340b5d5 fix(opencode): ACP File write should create the file if it doesn't exist (#12854) 2026-02-09 12:56:34 -06:00
Adam
ba740eaefd fix: locale routing 2026-02-09 12:52:06 -06:00
Adam
39c5da4405 fix(docs): dev docs links 2026-02-09 12:35:46 -06:00
Adam
83708c295c chore: cleanup 2026-02-09 12:20:09 -06:00
Adam
a84bdd7cd7 fix(app): incorrect workspace on new session 2026-02-09 12:19:04 -06:00
opencode-agent[bot]
110f6804fb chore: update nix node_modules hashes 2026-02-09 17:44:51 +00:00
opencode-agent[bot]
e5ec2f9991 chore: update nix node_modules hashes 2026-02-09 17:37:37 +00:00
opencode-agent[bot]
7bca3fbf18 chore: generate 2026-02-09 17:36:43 +00:00
opencode-agent[bot]
d578f80f00 chore: generate 2026-02-09 17:35:30 +00:00
Adam
dc53086c1e wip(docs): i18n (#12681) 2026-02-09 11:34:35 -06:00
Aiden Cline
f74c0339cc test: fix failing prompt test (#12847) 2026-02-09 11:25:22 -06:00
Jérôme Benoit
fb94b4f8e8 fix(nix): restore install script in fileset for desktop build (#12842) 2026-02-09 11:23:34 -06:00
Aiden Cline
8ad4768ecd tweak: adjust agent variant logic to not require exact match on model, and instead check if the variant is available for model (#12838) 2026-02-09 11:00:06 -06:00
Jérôme Benoit
24fd8c166d fix(nix): watch scripts in nix-hashes workflow (#12818) 2026-02-09 10:13:25 -06:00
Aiden Cline
a7c5d5ac4c Revert "feat(tui): restore footer to session view (#12245)" (#12836) 2026-02-09 10:08:26 -06:00
Adam
5be1202eea chore: cleanup 2026-02-09 09:58:55 -06:00
Joseph Campuzano
373b2270e7 fix(app): make keyboard focus visible in settings (#12612) 2026-02-09 09:12:06 -06:00
Dax
94d0c9940a Merge branch 'dev' into sqlite2 2026-02-09 10:04:55 -05:00
Adam
05355a6b5c fix(app): tooltip children focus loop 2026-02-09 07:43:36 -06:00
Adam
7ff51183ce chore: cleanup 2026-02-09 07:41:17 -06:00
Adam
bda0cbdec7 chore: cleanup 2026-02-09 07:38:06 -06:00
Adam
acc53d9f61 chore(app): cleanup 2026-02-09 07:38:06 -06:00
Adam
30f0d3b394 fix(app): update tab file contents on change 2026-02-09 07:38:06 -06:00
Adam
03f3029dc6 feat(app): polish Open in icon treatment
Bring in the Open in button-group and transparent icon updates from #12641 while keeping locale strings unchanged. Replace CSS inversion with dedicated light/dark Zed icon assets for cleaner theme handling.

Co-authored-by: Edin <86423329+edoedac0@users.noreply.github.com>
2026-02-09 07:38:05 -06:00
Anton Volkov
aed7bb8c09 chore: remove unused themes (#12753) 2026-02-09 13:33:00 +00:00
Adam
dd2d232a9d fix: add fallback for when crypto.randomUUID is unavailable
Closes #11452

Co-authored-by: Yo'av Moshe <bjesus@users.noreply.github.com>
2026-02-09 06:35:53 -06:00
Kit Langton
993ac55e39 fix(app): allow creating sessions on touch devices (#12765) 2026-02-09 05:55:54 -06:00
Israel Araújo de Oliveira
93a11ddedf feat(desktop): add native Wayland toggle on Linux (#11971)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-02-09 17:00:35 +08:00
Brendan Allan
94feb811ca app: include sandboxes in project unseen/error notifs 2026-02-09 16:51:04 +08:00
OpeOginni
b0ceec9b19 feat(desktop): persist currentSidecarUrl in state when isSidecar prop is true (#12792) 2026-02-09 07:56:26 +00:00
opencode-agent[bot]
40b111d92c chore: generate 2026-02-09 07:36:24 +00:00
Brendan Allan
520110e864 desktop: track currentSidecarUrl 2026-02-09 15:34:52 +08:00
Silvio Ney
d4a68b0f4e feat: exclude devtools from production builds (#12290)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-02-09 07:30:15 +00:00
opencode-agent[bot]
019cfd4a52 chore: generate 2026-02-09 07:18:48 +00:00
OpeOginni
687210a55d feat(desktop): add isSidecar prop to AppInterface and logic to persist sidecar server urls (#12366)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-02-09 07:18:06 +00:00
opencode-agent[bot]
b12eab782f chore: generate 2026-02-09 05:54:56 +00:00
Aiden Cline
99ea1351ce tweak: add new ContextOverflowError type (#12777) 2026-02-08 23:54:01 -06:00
fgonzalezurriola
d40dffb854 fix(provider): remove obsolete copilot model enablement instructions (#12739) 2026-02-08 22:43:52 -06:00
AdJIa
0cd52f830c fix: enable thinking for all reasoning models on alibaba-cn (DashScope) (#12772)
Co-authored-by: lujia <lujiax@zmn.cn>
2026-02-08 22:42:23 -06:00
Aiden Cline
62f38087b8 fix: parse mid stream openai responses style errors to prevent infinite retries for errors that should STOP execution (#12768) 2026-02-08 20:55:41 -06:00
Jérôme Benoit
79879b43ce refactor(nix): use native Bun APIs and propagate errors (#12694) 2026-02-08 19:43:10 -06:00
Alex Yaroshuk
a598ecac1f fix(app): localize "close tab" in command pallete (#12756) 2026-02-08 19:14:48 -06:00
Adam
9ac54adbb2 chore: cleanup 2026-02-08 16:23:24 -06:00
Adam
6490fb0148 fix(console): zen workspace translation cleanup 2026-02-08 16:22:39 -06:00
Adam
43811b62d2 chore: cleanup 2026-02-08 16:19:13 -06:00
Kit Langton
de0f4ef80b fix(layout): improve workspace header truncation and item interaction (#12655) 2026-02-08 18:42:04 +00:00
opencode-agent[bot]
9a7f54f21a chore: generate 2026-02-08 17:38:44 +00:00
Kit Langton
27c8a08144 ui: default TextField copy affordance to clipboard (#12714) 2026-02-08 11:37:59 -06:00
Frank
80c1c59ed3 wip: zen 2026-02-08 10:31:09 -05:00
Adam
7c6b8d7a8a fix(ui): context stale in prompt input (#12695) 2026-02-08 12:36:53 +00:00
Dax Raad
5952891b1e core: filter session list to show only sessions relevant to current directory location
When running from a subdirectory, the session list now shows only sessions

that belong to the current directory or its subdirectories, instead of

showing all sessions from the entire project.
2026-02-07 01:18:57 -05:00
Dax Raad
d7c8a3f50d Merge remote-tracking branch 'origin/dev' into sqlite2 2026-02-07 01:11:15 -05:00
Dax Raad
ce353819e8 Merge branch 'dev' into sqlite2 2026-02-05 23:50:20 -05:00
Dax Raad
2dae94e5a3 core: show visual progress bar during database migration so users can see real-time status of projects, sessions, and messages being converted 2026-02-05 23:21:37 -05:00
Dax Raad
c6adc19e41 Merge branch 'dev' into sqlite2 2026-02-05 17:54:46 -05:00
Dax Raad
ce56166510 core: suppress error output when auto-installing dependencies to prevent confusing error messages from appearing in the UI 2026-02-05 12:47:05 -05:00
Dax Raad
5911e4c06a Merge branch 'dev' into sqlite2 2026-02-05 12:39:31 -05:00
Dax Raad
42fb840f22 Merge branch 'dev' into sqlite2 2026-02-05 11:52:58 -05:00
Dax Raad
4dcfdf6572 Merge branch 'dev' into sqlite2 2026-02-04 22:44:49 -05:00
Dax Raad
25f3d6d5a9 Merge branch 'dev' into sqlite2 2026-02-04 22:37:08 -05:00
Dax Raad
e19a9e9614 core: fix migration of legacy messages and parts missing IDs in JSON body
Users upgrading from older versions where message and part IDs were only stored in filenames (not the JSON body) can now have their session data properly migrated to SQLite. This ensures no data loss when transitioning to the new storage format.
2026-02-04 22:35:26 -05:00
Dax Raad
fcc903489b Merge branch 'dev' into sqlite2 2026-02-03 15:51:09 -05:00
Dax
949e69a9bf Merge branch 'dev' into sqlite2 2026-02-02 23:36:00 -05:00
Dax
8c30f551e2 Merge branch 'dev' into sqlite2 2026-02-01 23:58:01 -05:00
opencode-agent[bot]
cb721497c1 chore: update nix node_modules hashes 2026-02-02 01:40:44 +00:00
Dax Raad
4ec6293054 fix: type errors in console-core and session
- Fix ExtractTablesWithRelations type compatibility with drizzle-orm beta
- Migrate Session.list() and Session.children() from Storage to SQLite
2026-02-01 20:31:02 -05:00
Dax Raad
b7a323355c fix: ExtractTablesWithRelations type parameter 2026-02-01 20:27:29 -05:00
Dax Raad
d4f053042c sync 2026-02-01 20:26:58 -05:00
Dax Raad
5f552534c7 Merge remote-tracking branch 'origin/sqlite2' into sqlite2 2026-02-01 20:26:24 -05:00
Dax Raad
ad5b790bb3 docs: simplify commit command by removing unnecessary instructions 2026-01-31 20:31:33 -05:00
Dax Raad
ed87341c4f core: fix storage directory existence check during json migration
Use fs.existsSync() instead of Bun.file().exists() since we're checking
for a directory, not a file.
2026-01-31 20:30:36 -05:00
opencode-agent[bot]
794ecab028 chore: update nix node_modules hashes 2026-02-01 01:20:05 +00:00
Dax Raad
eeb235724b Merge dev into sqlite2 2026-01-31 20:08:17 -05:00
Dax Raad
61084e7f6f sync 2026-01-31 16:32:37 -05:00
Dax Raad
200aef2eb3 sync 2026-01-31 16:28:07 -05:00
Dax Raad
f6e375a555 Merge origin/sqlite2 2026-01-31 16:10:11 -05:00
Dax Raad
db908deee5 Merge branch 'dev' into sqlite2 2026-01-31 16:08:47 -05:00
opencode-agent[bot]
7b72cc3a48 chore: update nix node_modules hashes 2026-01-30 16:21:25 +00:00
Dax Raad
b8cbfd48ec format 2026-01-30 11:17:33 -05:00
Dax Raad
498cbb2c26 core: split message part updates into delta events for smoother streaming
Streaming text and reasoning content now uses incremental delta events instead of sending full message parts on each update. This reduces bandwidth and improves real-time response smoothness in the TUI.
2026-01-30 11:16:30 -05:00
Dax Raad
d6fbd255b6 Merge branch 'dev' into sqlite2 2026-01-30 11:01:31 -05:00
Dax Raad
2de1c82bf7 Merge branch 'dev' into sqlite2 2026-01-30 10:03:47 -05:00
Dax Raad
34ebb3d051 Merge branch 'dev' into sqlite2 2026-01-29 16:07:20 -05:00
Dax Raad
9c3e3c1ab5 core: load migrations from timestamp-named directories instead of journal 2026-01-29 16:02:19 -05:00
Github Action
3ea499f04e chore: update nix node_modules hashes 2026-01-29 20:15:38 +00:00
Dax Raad
ab13c1d1c4 Merge branch 'dev' into sqlite2 2026-01-29 15:11:57 -05:00
Dax Raad
53b610c331 sync 2026-01-29 13:38:47 -05:00
Dax Raad
e3519356f2 sync 2026-01-29 13:32:20 -05:00
Dax Raad
2619acc0ff Merge branch 'dev' into sqlite2 2026-01-29 13:23:48 -05:00
Dax Raad
1bc45dc266 ignore: keep Chinese release notes label from blocking updates 2026-01-28 21:52:41 -05:00
Dax Raad
2e8feb1c78 core: significantly speed up data migration by optimizing SQLite settings and batch processing
Reduces migration time from minutes to seconds by enabling WAL mode, increasing batch size to 1000, and pre-scanning files upfront. Users upgrading to the SQLite backend will now see much faster startup times when their existing data is migrated.
2026-01-28 21:51:01 -05:00
Github Action
00e60899cc chore: update nix node_modules hashes 2026-01-28 23:49:18 +00:00
Dax Raad
30a918e9d4 Merge branch 'dev' into sqlite2 2026-01-28 18:45:12 -05:00
Dax Raad
ac16068140 Merge branch 'dev' into sqlite2 2026-01-27 17:45:05 -05:00
Dax Raad
19a41ab297 sync 2026-01-27 17:43:20 -05:00
Dax Raad
cd174d8cba sync 2026-01-27 16:57:51 -05:00
Dax Raad
246e901e42 Merge dev into sqlite2 2026-01-27 16:44:06 -05:00
Dax Raad
0ccef1b31f sync 2026-01-27 16:41:24 -05:00
Dax Raad
7706f5b6a8 core: switch commit command to kimi-k2.5 and improve worktree test reliability 2026-01-27 16:24:21 -05:00
Dax Raad
63e38555c9 sync 2026-01-27 15:33:44 -05:00
Dax Raad
f40685ab13 core: fix Drizzle ORM client initialization and type definitions 2026-01-27 12:38:38 -05:00
Dax Raad
a48a5a3462 core: migrate from custom JSON storage to standard Drizzle migrations to improve database reliability and performance
This replaces the previous manual JSON file system with standard Drizzle migrations, enabling:
- Proper database schema migrations with timestamp-based versioning
- Batched migration for faster migration of large datasets
- Better data integrity with proper table schemas instead of JSON blobs
- Easier database upgrades and rollback capabilities

Migration changes:
- Todo table now uses individual columns with composite PK instead of JSON blob
- Share table removes unused download share data
- Session diff table moved from database table to file storage
- All migrations now use proper Drizzle format with per-folder layout

Users will see a one-time migration on next run that migrates existing JSON data to the new SQLite database.
2026-01-27 12:36:05 -05:00
Dax Raad
5e1639de2b core: improve conversation loading performance by batching database queries
Reduces memory usage and speeds up conversation loading by using pagination
and inArray queries instead of loading all messages at once
2026-01-26 12:33:18 -05:00
Dax Raad
2b05833c32 core: ensure events publish reliably after database operations complete 2026-01-26 12:00:44 -05:00
Dax Raad
acdcf7fa88 core: remove dependency on remeda to simplify dependencies 2026-01-26 11:11:59 -05:00
Dax Raad
bf0754caeb sync 2026-01-26 10:35:53 -05:00
Dax Raad
4d50a32979 Merge dev into sqlite2 2026-01-26 08:53:01 -05:00
Dax Raad
57edb0ddc5 sync 2026-01-26 08:44:19 -05:00
Dax Raad
a614b78c6d tui: upgrade database migration system to drizzle migrator
Replaces custom migration system with drizzle-orm's built-in migrator, bundling migrations at build-time instead of runtime generation. This reduces bundle complexity and provides better integration with drizzle's migration tracking.
2026-01-25 22:27:04 -05:00
Github Action
b9f5a34247 chore: update nix node_modules hashes 2026-01-26 02:32:52 +00:00
Dax Raad
81b47a44e2 Merge branch 'dev' into sqlite2 2026-01-25 21:28:38 -05:00
Dax Raad
0c1c07467e Merge branch 'dev' into sqlite2 2026-01-25 20:18:36 -05:00
Dax Raad
105688bf90 sync 2026-01-25 20:16:56 -05:00
Dax Raad
1e7b4768b1 sync 2026-01-24 11:50:25 -05:00
818 changed files with 198422 additions and 6611 deletions

82
.github/workflows/docs-locale-sync.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
name: docs-locale-sync
on:
push:
branches:
- dev
paths:
- packages/web/src/content/docs/*.mdx
jobs:
sync-locales:
if: github.actor != 'opencode-agent[bot]'
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
id-token: write
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Compute changed English docs
id: changes
run: |
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'packages/web/src/content/docs/*.mdx' || true)
if [ -z "$FILES" ]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No English docs changed in push range"
exit 0
fi
echo "has_changes=true" >> "$GITHUB_OUTPUT"
{
echo "files<<EOF"
echo "$FILES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Sync locale docs with OpenCode
if: steps.changes.outputs.has_changes == 'true'
uses: sst/opencode/github@latest
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/gpt-5.2
agent: docs
prompt: |
Update localized docs to match the latest English docs changes.
Changed English doc files:
<changed_english_docs>
${{ steps.changes.outputs.files }}
</changed_english_docs>
Requirements:
1. Update all relevant locale docs under packages/web/src/content/docs/<locale>/ so they reflect these English page changes.
2. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update.
3. Keep locale docs structure aligned with their corresponding English pages.
4. Do not modify English source docs in packages/web/src/content/docs/*.mdx.
5. If no locale updates are needed, make no changes.
- name: Commit and push locale docs updates
if: steps.changes.outputs.has_changes == 'true'
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "No locale docs changes to commit"
exit 0
fi
git add -A
git commit -m "docs(i18n): sync locale docs from english changes"
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
git push origin HEAD:"$GITHUB_REF_NAME"

View File

@@ -12,6 +12,9 @@ on:
- "package.json"
- "packages/*/package.json"
- "flake.lock"
- "nix/node_modules.nix"
- "nix/scripts/**"
- "patches/**"
- ".github/workflows/nix-hashes.yml"
jobs:

View File

@@ -16,15 +16,12 @@ wip:
For anything in the packages/web use the docs: prefix.
For anything in the packages/app use the ignore: prefix.
prefer to explain WHY something was done from an end user perspective instead of
WHAT was done.
do not do generic messages like "improved agent experience" be very specific
about what user facing changes were made
if there are changes do a git pull --rebase
if there are conflicts DO NOT FIX THEM. notify me and I will fix them
## GIT DIFF

View File

@@ -110,3 +110,4 @@ const table = sqliteTable("session", {
- Avoid mocks as much as possible
- Test actual implementation, do not duplicate logic into tests
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.

611
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -135,6 +135,16 @@ const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS8"),
new sst.Secret("ZEN_MODELS9"),
new sst.Secret("ZEN_MODELS10"),
new sst.Secret("ZEN_MODELS11"),
new sst.Secret("ZEN_MODELS12"),
new sst.Secret("ZEN_MODELS13"),
new sst.Secret("ZEN_MODELS14"),
new sst.Secret("ZEN_MODELS15"),
new sst.Secret("ZEN_MODELS16"),
new sst.Secret("ZEN_MODELS17"),
new sst.Secret("ZEN_MODELS18"),
new sst.Secret("ZEN_MODELS19"),
new sst.Secret("ZEN_MODELS20"),
]
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")

View File

@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-1IpZnnN6+acCcV0AgO4OVdvgf4TFBFId5dms5W5ecA0=",
"aarch64-linux": "sha256-TKmPhXokOav46ucP9AFwHGgKmB9CdGCcUtwqUtLlzG4=",
"aarch64-darwin": "sha256-xJQuw3+QHYnlClDrafQKPQyR+aqyAEofvYYjCowHDps=",
"x86_64-darwin": "sha256-ywU3Oka2QNGKu/HI+//3bdYJ9qo1N7K5Wr2vpTgSM/g="
"x86_64-linux": "sha256-cvRBvHRuunNjF07c4GVHl5rRgoTn1qfI/HdJWtOV63M=",
"aarch64-linux": "sha256-DJUI4pMZ7wQTnyOiuDHALmZz7FZtrTbzRzCuNOShmWE=",
"aarch64-darwin": "sha256-JnkqDwuC7lNsjafV+jOGfvs8K1xC8rk5CTOW+spjiCA=",
"x86_64-darwin": "sha256-GBeTqq2vDn/mXplYNglrAT2xajjFVzB4ATHnMS0j7z4="
}
}

View File

@@ -30,7 +30,7 @@ stdenvNoCC.mkDerivation {
../bun.lock
../package.json
../patches
../install
../install # required by desktop build (cli.rs include_str!)
]
);
};

View File

@@ -1,27 +1,32 @@
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
import { join, relative } from "path"
type SemverLike = {
valid: (value: string) => string | null
rcompare: (left: string, right: string) => number
}
type Entry = {
dir: string
version: string
label: string
}
async function isDirectory(path: string) {
try {
const info = await lstat(path)
return info.isDirectory()
} catch {
return false
}
}
const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x")
const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
const linkRoot = join(bunRoot, "node_modules")
const directories = (await readdir(bunRoot)).sort()
const versions = new Map<string, Entry[]>()
for (const entry of directories) {
const full = join(bunRoot, entry)
const info = await lstat(full)
if (!info.isDirectory()) {
if (!(await isDirectory(full))) {
continue
}
const parsed = parseEntry(entry)
@@ -29,37 +34,23 @@ for (const entry of directories) {
continue
}
const list = versions.get(parsed.name) ?? []
list.push({ dir: full, version: parsed.version, label: entry })
list.push({ dir: full, version: parsed.version })
versions.set(parsed.name, list)
}
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
| SemverLike
| {
default: SemverLike
}
const semver = "default" in semverModule ? semverModule.default : semverModule
const selections = new Map<string, Entry>()
for (const [slug, list] of versions) {
list.sort((a, b) => {
const left = semver.valid(a.version)
const right = semver.valid(b.version)
if (left && right) {
const delta = semver.rcompare(left, right)
if (delta !== 0) {
return delta
}
}
if (left && !right) {
return -1
}
if (!left && right) {
return 1
}
const aValid = isValidSemver(a.version)
const bValid = isValidSemver(b.version)
if (aValid && bValid) return -Bun.semver.order(a.version, b.version)
if (aValid) return -1
if (bValid) return 1
return b.version.localeCompare(a.version)
})
selections.set(slug, list[0])
const first = list[0]
if (first) selections.set(slug, first)
}
await rm(linkRoot, { recursive: true, force: true })
@@ -77,10 +68,7 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
await mkdir(parent, { recursive: true })
const linkPath = join(parent, leaf)
const desired = join(entry.dir, "node_modules", slug)
const exists = await lstat(desired)
.then((info) => info.isDirectory())
.catch(() => false)
if (!exists) {
if (!(await isDirectory(desired))) {
continue
}
const relativeTarget = relative(parent, desired)

View File

@@ -8,7 +8,7 @@ type PackageManifest = {
const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
const bunEntries = (await safeReadDir(bunRoot)).sort()
const bunEntries = (await readdir(bunRoot)).sort()
let rewritten = 0
for (const entry of bunEntries) {
@@ -45,11 +45,11 @@ for (const entry of bunEntries) {
}
}
console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`)
console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`)
async function collectPackages(modulesRoot: string) {
const found: string[] = []
const topLevel = (await safeReadDir(modulesRoot)).sort()
const topLevel = (await readdir(modulesRoot)).sort()
for (const name of topLevel) {
if (name === ".bin" || name === ".bun") {
continue
@@ -59,7 +59,7 @@ async function collectPackages(modulesRoot: string) {
continue
}
if (name.startsWith("@")) {
const scoped = (await safeReadDir(full)).sort()
const scoped = (await readdir(full)).sort()
for (const child of scoped) {
const scopedDir = join(full, child)
if (await isDirectory(scopedDir)) {
@@ -121,14 +121,6 @@ async function isDirectory(path: string) {
}
}
async function safeReadDir(path: string) {
try {
return await readdir(path)
} catch {
return []
}
}
function normalizeBinName(name: string) {
const slash = name.lastIndexOf("/")
if (slash >= 0) {

View File

@@ -35,11 +35,13 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.1.0-beta.10",
"@pierre/diffs": "1.0.2",
"@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.12-a5629fb",
"drizzle-orm": "1.0.0-beta.12-a5629fb",
"ai": "5.0.124",
"hono": "4.10.7",
"hono-openapi": "1.1.2",

View File

@@ -84,7 +84,7 @@ function ServerKey(props: ParentProps) {
)
}
export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element }) {
export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) {
const platform = usePlatform()
const stored = (() => {
@@ -106,7 +106,7 @@ export function AppInterface(props: { defaultUrl?: string; children?: JSX.Elemen
}
return (
<ServerProvider defaultUrl={defaultServerUrl()}>
<ServerProvider defaultUrl={defaultServerUrl()} isSidecar={props.isSidecar}>
<ServerKey>
<GlobalSDKProvider>
<GlobalSyncProvider>

View File

@@ -31,7 +31,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
const dataUrl = reader.result as string
const attachment: ImageAttachmentPart = {
type: "image",
id: crypto.randomUUID(),
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
filename: file.name,
mime: file.type,
dataUrl,

View File

@@ -137,7 +137,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const projectDirectory = sdk.directory
const isNewSession = !params.id
const worktreeSelection = input.newSessionWorktree ?? "main"
const worktreeSelection = input.newSessionWorktree || "main"
let sessionDirectory = projectDirectory
let client = sdk.client

View File

@@ -79,15 +79,16 @@ describe("getSessionContextMetrics", () => {
expect(metrics.context?.usage).toBeNull()
})
test("memoizes by message and provider array identity", () => {
test("recomputes when message array is mutated in place", () => {
const messages = [assistant("a1", { input: 10, output: 10, reasoning: 10, read: 10, write: 10 }, 0.25)]
const providers = [{ id: "openai", models: {} }]
const one = getSessionContextMetrics(messages, providers)
messages.push(assistant("a2", { input: 100, output: 20, reasoning: 0, read: 0, write: 0 }, 0.75))
const two = getSessionContextMetrics(messages, providers)
const three = getSessionContextMetrics([...messages], providers)
expect(two).toBe(one)
expect(three).not.toBe(one)
expect(one.context?.message.id).toBe("a1")
expect(two.context?.message.id).toBe("a2")
expect(two.totalCost).toBe(1)
})
})

View File

@@ -34,8 +34,6 @@ type Metrics = {
context: Context | undefined
}
const cache = new WeakMap<Message[], WeakMap<Provider[], Metrics>>()
const tokenTotal = (msg: AssistantMessage) => {
return msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + msg.tokens.cache.read + msg.tokens.cache.write
}
@@ -80,15 +78,5 @@ const build = (messages: Message[], providers: Provider[]): Metrics => {
}
export function getSessionContextMetrics(messages: Message[], providers: Provider[]) {
const byProvider = cache.get(messages)
if (byProvider) {
const hit = byProvider.get(providers)
if (hit) return hit
}
const value = build(messages, providers)
const next = byProvider ?? new WeakMap<Provider[], Metrics>()
next.set(providers, value)
if (!byProvider) cache.set(messages, next)
return value
return build(messages, providers)
}

View File

@@ -112,21 +112,35 @@ export function SessionHeader() {
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ finder: true })
const apps = createMemo(() => {
if (os() === "macos") return MAC_APPS
if (os() === "windows") return WINDOWS_APPS
return LINUX_APPS
})
const fileManager = createMemo(() => {
if (os() === "macos") return { label: "Finder", icon: "finder" as const }
if (os() === "windows") return { label: "File Explorer", icon: "file-explorer" as const }
return { label: "File Manager", icon: "finder" as const }
})
createEffect(() => {
if (platform.platform !== "desktop") return
if (!platform.checkAppExists) return
const list = os()
const apps = list === "macos" ? MAC_APPS : list === "windows" ? WINDOWS_APPS : list === "linux" ? LINUX_APPS : []
if (apps.length === 0) return
const list = apps()
setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial<Record<OpenApp, boolean>>)
void Promise.all(
apps.map((app) =>
Promise.resolve(platform.checkAppExists?.(app.openWith)).then((value) => {
const ok = Boolean(value)
console.debug(`[session-header] App "${app.label}" (${app.openWith}): ${ok ? "exists" : "does not exist"}`)
return [app.id, ok] as const
}),
list.map((app) =>
Promise.resolve(platform.checkAppExists?.(app.openWith))
.then((value) => Boolean(value))
.catch(() => false)
.then((ok) => {
console.debug(`[session-header] App "${app.label}" (${app.openWith}): ${ok ? "exists" : "does not exist"}`)
return [app.id, ok] as const
}),
),
).then((entries) => {
setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>)
@@ -134,23 +148,23 @@ export function SessionHeader() {
})
const options = createMemo(() => {
if (os() === "macos") {
return [{ id: "finder", label: "Finder", icon: "finder" }, ...MAC_APPS.filter((app) => exists[app.id])] as const
}
if (os() === "windows") {
return [
{ id: "finder", label: "File Explorer", icon: "file-explorer" },
...WINDOWS_APPS.filter((app) => exists[app.id]),
] as const
}
return [
{ id: "finder", label: "File Manager", icon: "finder" },
...LINUX_APPS.filter((app) => exists[app.id]),
{ id: "finder", label: fileManager().label, icon: fileManager().icon },
...apps().filter((app) => exists[app.id]),
] as const
})
type OpenIcon = OpenApp | "file-explorer"
const base = new Set<OpenIcon>(["finder", "vscode", "cursor", "zed"])
const size = (id: OpenIcon) => (base.has(id) ? "size-4" : "size-[19px]")
const checksReady = createMemo(() => {
if (platform.platform !== "desktop") return true
if (!platform.checkAppExists) return true
const list = apps()
return list.every((app) => exists[app.id] !== undefined)
})
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
@@ -158,6 +172,7 @@ export function SessionHeader() {
createEffect(() => {
if (platform.platform !== "desktop") return
if (!checksReady()) return
const value = prefs.app
if (options().some((o) => o.id === value)) return
setPrefs("app", options()[0]?.id ?? "finder")
@@ -283,7 +298,7 @@ export function SessionHeader() {
<Portal mount={mount()}>
<button
type="button"
class="hidden md:flex w-[320px] max-w-full min-w-0 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-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
class="hidden md:flex w-[320px] max-w-full min-w-0 h-[24px] px-2 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-base bg-surface-panel transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
onClick={() => command.trigger("file.open")}
aria-label={language.t("session.header.searchFiles")}
>
@@ -294,7 +309,11 @@ export function SessionHeader() {
</span>
</div>
<Show when={hotkey()}>{(keybind) => <Keybind class="shrink-0">{keybind()}</Keybind>}</Show>
<Show when={hotkey()}>
{(keybind) => (
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind()}</Keybind>
)}
</Show>
</button>
</Portal>
)}
@@ -303,6 +322,7 @@ export function SessionHeader() {
{(mount) => (
<Portal mount={mount()}>
<div class="flex items-center gap-3">
<StatusPopover />
<Show when={projectDirectory()}>
<div class="hidden xl:flex items-center">
<Show
@@ -322,62 +342,68 @@ export function SessionHeader() {
}
>
<div class="flex items-center">
<Button
variant="ghost"
class="rounded-sm h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none rounded-r-none"
onClick={() => openDir(current().id)}
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
>
<AppIcon id={current().icon} class="size-5" />
<span class="text-12-regular text-text-strong">
{language.t("session.header.open.action", { app: current().label })}
</span>
</Button>
<DropdownMenu>
<DropdownMenu.Trigger
as={IconButton}
icon="chevron-down"
<div class="flex h-[24px] box-border items-center rounded-md border border-border-base bg-surface-panel overflow-hidden">
<Button
variant="ghost"
class="rounded-sm h-[24px] w-auto px-1.5 border-none shadow-none rounded-l-none data-[expanded]:bg-surface-raised-base-active"
aria-label={language.t("session.header.open.menu")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content placement="bottom-end" gutter={6}>
<DropdownMenu.Group>
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
<DropdownMenu.RadioGroup
value={prefs.app}
onChange={(value) => {
if (!OPEN_APPS.includes(value as OpenApp)) return
setPrefs("app", value as OpenApp)
}}
>
{options().map((o) => (
<DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
<AppIcon id={o.icon} class="size-5" />
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
<DropdownMenu.ItemIndicator>
<Icon name="check-small" size="small" class="text-icon-weak" />
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
))}
</DropdownMenu.RadioGroup>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={copyPath}>
<Icon name="copy" size="small" class="text-icon-weak" />
<DropdownMenu.ItemLabel>
{language.t("session.header.open.copyPath")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
class="rounded-none h-full py-0 pr-3 pl-2 gap-1.5 border-none shadow-none"
onClick={() => openDir(current().id)}
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
>
<div class="flex size-5 shrink-0 items-center justify-center">
<AppIcon id={current().icon} class="size-4" />
</div>
<span class="text-12-regular text-text-strong">Open</span>
</Button>
<div class="self-stretch w-px bg-border-base/70" />
<DropdownMenu gutter={6} placement="bottom-end">
<DropdownMenu.Trigger
as={IconButton}
icon="chevron-down"
variant="ghost"
class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active"
aria-label={language.t("session.header.open.menu")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
<DropdownMenu.RadioGroup
value={prefs.app}
onChange={(value) => {
if (!OPEN_APPS.includes(value as OpenApp)) return
setPrefs("app", value as OpenApp)
}}
>
{options().map((o) => (
<DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
<div class="flex size-5 shrink-0 items-center justify-center">
<AppIcon id={o.icon} class={size(o.icon)} />
</div>
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
<DropdownMenu.ItemIndicator>
<Icon name="check-small" size="small" class="text-icon-weak" />
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
))}
</DropdownMenu.RadioGroup>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={copyPath}>
<div class="flex size-5 shrink-0 items-center justify-center">
<Icon name="copy" size="small" class="text-icon-weak" />
</div>
<DropdownMenu.ItemLabel>
{language.t("session.header.open.copyPath")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
</div>
</Show>
</div>
</Show>
<StatusPopover />
<Show when={showShare()}>
<div class="flex items-center">
<Popover
@@ -393,8 +419,9 @@ export function SessionHeader() {
class="rounded-xl [&_[data-slot=popover-close-button]]:hidden"
triggerAs={Button}
triggerProps={{
variant: "secondary",
class: "rounded-sm h-[24px] px-3",
variant: "ghost",
class:
"rounded-md h-[24px] px-3 border border-border-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-active",
classList: { "rounded-r-none": shareUrl() !== undefined },
style: { scale: 1 },
}}
@@ -420,7 +447,14 @@ export function SessionHeader() {
}
>
<div class="flex flex-col gap-2">
<TextField value={shareUrl() ?? ""} readOnly copyable tabIndex={-1} class="w-full" />
<TextField
value={shareUrl() ?? ""}
readOnly
copyable
copyKind="link"
tabIndex={-1}
class="w-full"
/>
<div class="grid grid-cols-2 gap-2">
<Button
size="large"
@@ -459,8 +493,8 @@ export function SessionHeader() {
>
<IconButton
icon={state.copied ? "check" : "link"}
variant="secondary"
class="rounded-l-none"
variant="ghost"
class="rounded-l-none h-[24px] border border-border-base bg-surface-panel shadow-none"
onClick={copyLink}
disabled={state.unshare}
aria-label={

View File

@@ -1,6 +1,7 @@
import { Show, createMemo } from "solid-js"
import { DateTime } from "luxon"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { useLanguage } from "@/context/language"
import { Icon } from "@opencode-ai/ui/icon"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
@@ -15,6 +16,7 @@ interface NewSessionViewProps {
export function NewSessionView(props: NewSessionViewProps) {
const sync = useSync()
const sdk = useSDK()
const language = useLanguage()
const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
@@ -24,11 +26,11 @@ export function NewSessionView(props: NewSessionViewProps) {
if (options().includes(selection)) return selection
return MAIN_WORKTREE
})
const projectRoot = createMemo(() => sync.project?.worktree ?? sync.data.path.directory)
const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory)
const isWorktree = createMemo(() => {
const project = sync.project
if (!project) return false
return sync.data.path.directory !== project.worktree
return sdk.directory !== project.worktree
})
const label = (value: string) => {
@@ -45,7 +47,7 @@ export function NewSessionView(props: NewSessionViewProps) {
}
return (
<div class="size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6 pb-[calc(var(--prompt-height,11.25rem)+64px)]">
<div class="size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto 2xl:max-w-[1000px] px-6 pb-[calc(var(--prompt-height,11.25rem)+64px)]">
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />

View File

@@ -1,8 +1,10 @@
import { Component, createMemo, type JSX } from "solid-js"
import { Component, Show, createEffect, createMemo, createResource, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
@@ -40,6 +42,8 @@ export const SettingsGeneral: Component = () => {
checking: false,
})
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
const check = () => {
if (!platform.checkUpdate) return
setStore("checking", true)
@@ -410,13 +414,49 @@ export const SettingsGeneral: Component = () => {
</SettingsRow>
</div>
</div>
<Show when={linux()}>
{(_) => {
const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest)
const onChange = (checked: boolean) =>
platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch())
return (
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsRow
title={
<div class="flex items-center gap-2">
<span>{language.t("settings.general.row.wayland.title")}</span>
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
<span class="text-text-weak">
<Icon name="help" size="small" />
</span>
</Tooltip>
</div>
}
description={language.t("settings.general.row.wayland.description")}
>
<div data-action="settings-wayland">
<Switch checked={value() === "wayland"} onChange={onChange} />
</div>
</SettingsRow>
</div>
</div>
)
}}
</Show>
</div>
</div>
)
}
interface SettingsRowProps {
title: string
title: string | JSX.Element
description: string | JSX.Element
children: JSX.Element
}

View File

@@ -141,7 +141,7 @@ export function StatusPopover() {
triggerProps={{
variant: "ghost",
class:
"rounded-sm w-[75px] h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none data-[expanded]:bg-surface-raised-base-active",
"rounded-md h-[24px] px-3 gap-2 border border-border-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-active",
style: { scale: 1 },
}}
trigger={

View File

@@ -53,7 +53,7 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
const add = (input: Omit<LineComment, "id" | "time">) => {
const next: LineComment = {
id: crypto.randomUUID(),
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
time: Date.now(),
...input,
}

View File

@@ -7,6 +7,7 @@ import { getFilename } from "@opencode-ai/util/path"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { createPathHelpers } from "./file/path"
import {
approxBytes,
@@ -50,9 +51,11 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
useSync()
const params = useParams()
const language = useLanguage()
const layout = useLayout()
const scope = createMemo(() => sdk.directory)
const path = createPathHelpers(scope)
const tabs = layout.tabs(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const inflight = new Map<string, Promise<void>>()
const [store, setStore] = createStore<{
@@ -183,6 +186,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
invalidateFromWatcher(e.details, {
normalize: path.normalize,
hasFile: (file) => Boolean(store.file[file]),
isOpen: (file) => tabs.all().some((tab) => path.pathFromTab(tab) === file),
loadFile: (file) => {
void load(file, { force: true })
},

View File

@@ -27,6 +27,37 @@ describe("file watcher invalidation", () => {
expect(refresh).toEqual(["src"])
})
test("reloads files that are open in tabs", () => {
const loads: string[] = []
invalidateFromWatcher(
{
type: "file.watcher.updated",
properties: {
file: "src/open.ts",
event: "change",
},
},
{
normalize: (input) => input,
hasFile: () => false,
isOpen: (path) => path === "src/open.ts",
loadFile: (path) => loads.push(path),
node: () => ({
path: "src/open.ts",
type: "file",
name: "open.ts",
absolute: "/repo/src/open.ts",
ignored: false,
}),
isDirLoaded: () => false,
refreshDir: () => {},
},
)
expect(loads).toEqual(["src/open.ts"])
})
test("refreshes only changed loaded directory nodes", () => {
const refresh: string[] = []

View File

@@ -8,6 +8,7 @@ type WatcherEvent = {
type WatcherOps = {
normalize: (input: string) => string
hasFile: (path: string) => boolean
isOpen?: (path: string) => boolean
loadFile: (path: string) => void
node: (path: string) => FileNode | undefined
isDirLoaded: (path: string) => boolean
@@ -27,7 +28,7 @@ export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) {
if (!path) return
if (path.startsWith(".git/")) return
if (ops.hasFile(path)) {
if (ops.hasFile(path) || ops.isOpen?.(path)) {
ops.loadFile(path)
}

View File

@@ -231,6 +231,24 @@ export function applyDirectoryEvent(input: {
}
break
}
case "message.part.delta": {
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
const parts = input.store.part[props.messageID]
if (!parts) break
const result = Binary.search(parts, props.partID, (p) => p.id)
if (!result.found) break
input.setStore(
"part",
props.messageID,
produce((draft) => {
const part = draft[result.index]
const field = props.field as keyof typeof part
const existing = part[field] as string | undefined
;(part[field] as string) = (existing ?? "") + props.delta
}),
)
break
}
case "vcs.branch.updated": {
const props = event.properties as { branch: string }
const next = { branch: props.branch }

View File

@@ -57,6 +57,12 @@ export type Platform = {
/** Set the default server URL to use on app startup (platform-specific) */
setDefaultServerUrl?(url: string | null): Promise<void> | void
/** Get the preferred display backend (desktop only) */
getDisplayBackend?(): Promise<DisplayBackend | null> | DisplayBackend | null
/** Set the preferred display backend (desktop only) */
setDisplayBackend?(backend: DisplayBackend): Promise<void>
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
parseMarkdown?(markdown: string): Promise<string>
@@ -70,6 +76,8 @@ export type Platform = {
readClipboardImage?(): Promise<File | null>
}
export type DisplayBackend = "auto" | "wayland"
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
name: "Platform",
init: (props: { value: Platform }) => {

View File

@@ -28,13 +28,14 @@ function projectsKey(url: string) {
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server",
init: (props: { defaultUrl: string }) => {
init: (props: { defaultUrl: string; isSidecar?: boolean }) => {
const platform = usePlatform()
const [store, setStore, _, ready] = persisted(
Persist.global("server", ["server.v3"]),
createStore({
list: [] as string[],
currentSidecarUrl: "",
projects: {} as Record<string, StoredProject[]>,
lastProject: {} as Record<string, string>,
}),
@@ -59,7 +60,13 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const fallback = normalizeServerUrl(props.defaultUrl)
if (fallback && url === fallback) {
setState("active", url)
batch(() => {
if (!store.list.includes(url)) {
// Add the fallback url to the list if it's not already in the list
setStore("list", store.list.length, url)
}
setState("active", url)
})
return
}
@@ -89,7 +96,20 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
if (state.active) return
const url = normalizeServerUrl(props.defaultUrl)
if (!url) return
setState("active", url)
batch(() => {
// Remove the previous startup sidecar url
if (store.currentSidecarUrl) {
remove(store.currentSidecarUrl)
}
// Add the new sidecar url
if (props.isSidecar && props.defaultUrl) {
add(props.defaultUrl)
setStore("currentSidecarUrl", props.defaultUrl)
}
setState("active", url)
})
})
const isReady = createMemo(() => ready() && !!state.active)

View File

@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "جلسة جديدة",
"command.file.open": "فتح ملف",
"command.tab.close": "إغلاق علامة التبويب",
"command.context.addSelection": "إضافة التحديد إلى السياق",
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
"command.input.focus": "التركيز على حقل الإدخال",

View File

@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Nova sessão",
"command.file.open": "Abrir arquivo",
"command.tab.close": "Fechar aba",
"command.context.addSelection": "Adicionar seleção ao contexto",
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
"command.input.focus": "Focar entrada",

View File

@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Ny session",
"command.file.open": "Åbn fil",
"command.tab.close": "Luk fane",
"command.context.addSelection": "Tilføj markering til kontekst",
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
"command.input.focus": "Fokuser inputfelt",

View File

@@ -48,6 +48,7 @@ export const dict = {
"command.session.new": "Neue Sitzung",
"command.file.open": "Datei öffnen",
"command.tab.close": "Tab schließen",
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
"command.input.focus": "Eingabefeld fokussieren",

View File

@@ -588,6 +588,7 @@ export const dict = {
"settings.general.section.notifications": "System notifications",
"settings.general.section.updates": "Updates",
"settings.general.section.sounds": "Sound effects",
"settings.general.section.display": "Display",
"settings.general.row.language.title": "Language",
"settings.general.row.language.description": "Change the display language for OpenCode",
@@ -598,6 +599,11 @@ export const dict = {
"settings.general.row.font.title": "Font",
"settings.general.row.font.description": "Customise the mono font used in code blocks",
"settings.general.row.wayland.title": "Use native Wayland",
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
"settings.general.row.wayland.tooltip":
"On Linux with mixed refresh-rate monitors, native Wayland can be more stable.",
"settings.general.row.releaseNotes.title": "Release notes",
"settings.general.row.releaseNotes.description": "Show What's New popups after updates",

View File

@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Nueva sesión",
"command.file.open": "Abrir archivo",
"command.tab.close": "Cerrar pestaña",
"command.context.addSelection": "Añadir selección al contexto",
"command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual",
"command.input.focus": "Enfocar entrada",

View File

@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Nouvelle session",
"command.file.open": "Ouvrir un fichier",
"command.tab.close": "Fermer l'onglet",
"command.context.addSelection": "Ajouter la sélection au contexte",
"command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel",
"command.input.focus": "Focus input",

View File

@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "新しいセッション",
"command.file.open": "ファイルを開く",
"command.tab.close": "タブを閉じる",
"command.context.addSelection": "選択範囲をコンテキストに追加",
"command.context.addSelection.description": "現在のファイルから選択した行を追加",
"command.input.focus": "入力欄にフォーカス",

View File

@@ -48,6 +48,7 @@ export const dict = {
"command.session.new": "새 세션",
"command.file.open": "파일 열기",
"command.tab.close": "탭 닫기",
"command.context.addSelection": "선택 영역을 컨텍스트에 추가",
"command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가",
"command.input.focus": "입력창 포커스",

View File

@@ -47,6 +47,7 @@ export const dict = {
"command.session.new": "Ny sesjon",
"command.file.open": "Åpne fil",
"command.tab.close": "Lukk fane",
"command.context.addSelection": "Legg til markering i kontekst",
"command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil",
"command.input.focus": "Fokuser inndata",

View File

@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Nowa sesja",
"command.file.open": "Otwórz plik",
"command.tab.close": "Zamknij kartę",
"command.context.addSelection": "Dodaj zaznaczenie do kontekstu",
"command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku",
"command.input.focus": "Fokus na pole wejściowe",

View File

@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Новая сессия",
"command.file.open": "Открыть файл",
"command.tab.close": "Закрыть вкладку",
"command.context.addSelection": "Добавить выделение в контекст",
"command.context.addSelection.description": "Добавить выбранные строки из текущего файла",
"command.input.focus": "Фокус на поле ввода",

View File

@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "เซสชันใหม่",
"command.file.open": "เปิดไฟล์",
"command.tab.close": "ปิดแท็บ",
"command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท",
"command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน",
"command.input.focus": "โฟกัสช่องป้อนข้อมูล",

View File

@@ -48,6 +48,7 @@ export const dict = {
"command.session.new": "新建会话",
"command.file.open": "打开文件",
"command.tab.close": "关闭标签页",
"command.context.addSelection": "将所选内容添加到上下文",
"command.context.addSelection.description": "添加当前文件中选中的行",
"command.input.focus": "聚焦输入框",

View File

@@ -48,6 +48,7 @@ export const dict = {
"command.session.new": "新增工作階段",
"command.file.open": "開啟檔案",
"command.tab.close": "關閉分頁",
"command.context.addSelection": "將選取內容加入上下文",
"command.context.addSelection.description": "加入目前檔案中選取的行",
"command.input.focus": "聚焦輸入框",

View File

@@ -1,3 +1,3 @@
export { PlatformProvider, type Platform } from "./context/platform"
export { PlatformProvider, type Platform, type DisplayBackend } from "./context/platform"
export { AppBaseProviders, AppInterface } from "./app"
export { useCommand } from "./context/command"

View File

@@ -21,8 +21,11 @@ const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
const notification = useNotification()
const unseenCount = createMemo(() => notification.project.unseenCount(props.project.worktree))
const hasError = createMemo(() => notification.project.unseenHasError(props.project.worktree))
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
const unseenCount = createMemo(() =>
dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
)
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
return (
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>

View File

@@ -2,6 +2,7 @@ import { useNavigate, useParams } from "@solidjs/router"
import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { createSortable } from "@thisbeyond/solid-dnd"
import { createMediaQuery } from "@solid-primitives/media"
import { base64Encode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path"
import { Button } from "@opencode-ai/ui/button"
@@ -114,7 +115,8 @@ export const SortableWorkspace = (props: {
const busy = createMemo(() => props.ctx.isBusy(props.directory))
const wasBusy = createMemo((prev) => prev || busy(), false)
const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy())
const showNew = createMemo(() => !loading() && (sessions().length === 0 || (active() && !params.id)))
const touch = createMediaQuery("(hover: none)")
const showNew = createMemo(() => !loading() && (touch() || sessions().length === 0 || (active() && !params.id)))
const loadMore = async () => {
setWorkspaceStore("limit", (limit) => limit + 5)
await globalSync.project.loadSessions(props.directory)
@@ -167,11 +169,9 @@ export const SortableWorkspace = (props: {
openOnDblClick={false}
/>
</Show>
<Icon
name={open() ? "chevron-down" : "chevron-right"}
size="small"
class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/workspace:opacity-100 group-focus-within/workspace:opacity-100"
/>
<div class="flex items-center justify-center shrink-0 overflow-hidden w-0 opacity-0 transition-all duration-200 group-hover/workspace:w-3.5 group-hover/workspace:opacity-100 group-focus-within/workspace:w-3.5 group-focus-within/workspace:opacity-100">
<Icon name={open() ? "chevron-down" : "chevron-right"} size="small" class="text-icon-base" />
</div>
</div>
)
@@ -196,7 +196,9 @@ export const SortableWorkspace = (props: {
when={workspaceEditActive()}
fallback={
<Collapsible.Trigger
class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover"
class={`flex items-center justify-between w-full pl-2 py-1.5 rounded-md hover:bg-surface-raised-base-hover transition-[padding] duration-200 ${
menu.open ? "pr-16" : "pr-2"
} group-hover/workspace:pr-16 group-focus-within/workspace:pr-16`}
data-action="workspace-toggle"
data-workspace={base64Encode(props.directory)}
>
@@ -204,7 +206,13 @@ export const SortableWorkspace = (props: {
</Collapsible.Trigger>
}
>
<div class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md">{header()}</div>
<div
class={`flex items-center justify-between w-full pl-2 py-1.5 rounded-md transition-[padding] duration-200 ${
menu.open ? "pr-16" : "pr-2"
} group-hover/workspace:pr-16 group-focus-within/workspace:pr-16`}
>
{header()}
</div>
</Show>
<div
class="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5 transition-opacity"
@@ -264,23 +272,25 @@ export const SortableWorkspace = (props: {
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<Tooltip value={language.t("command.session.new")} placement="top">
<IconButton
icon="plus-small"
variant="ghost"
class="size-6 rounded-md opacity-0 pointer-events-none group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto"
data-action="workspace-new-session"
data-workspace={base64Encode(props.directory)}
aria-label={language.t("command.session.new")}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
props.ctx.setHoverSession(undefined)
props.ctx.clearHoverProjectSoon()
navigate(`/${slug()}/session`)
}}
/>
</Tooltip>
<Show when={!touch()}>
<Tooltip value={language.t("command.session.new")} placement="top">
<IconButton
icon="plus-small"
variant="ghost"
class="size-6 rounded-md opacity-0 pointer-events-none group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto"
data-action="workspace-new-session"
data-workspace={base64Encode(props.directory)}
aria-label={language.t("command.session.new")}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
props.ctx.setHoverSession(undefined)
props.ctx.clearHoverProjectSoon()
navigate(`/${slug()}/session`)
}}
/>
</Tooltip>
</Show>
</div>
</div>
</div>

View File

@@ -591,7 +591,7 @@ export default function Page() {
const newSessionWorktree = createMemo(() => {
if (store.newSessionWorktree === "create") return "create"
const project = sync.project
if (project && sync.data.path.directory !== project.worktree) return sync.data.path.directory
if (project && sdk.directory !== project.worktree) return sdk.directory
return "main"
})
@@ -1647,7 +1647,7 @@ export default function Page() {
const target = value === "main" ? sync.project?.worktree : value
if (!target) return
if (target === sync.data.path.directory) return
if (target === sdk.directory) return
layout.projects.open(target)
navigate(`/${base64Encode(target)}/session`)
}}

View File

@@ -179,7 +179,7 @@ export function MessageTimeline(props: {
"sticky top-0 z-30 bg-background-stronger": true,
"w-full": true,
"px-4 md:px-6": true,
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered,
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
}}
>
<div class="h-10 w-full flex items-center justify-between gap-2">
@@ -278,7 +278,7 @@ export function MessageTimeline(props: {
class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
classList={{
"w-full": true,
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered,
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
"mt-0.5": props.centered,
"mt-0": !props.centered,
}}
@@ -321,7 +321,7 @@ export function MessageTimeline(props: {
}}
classList={{
"min-w-0 w-full max-w-full": true,
"md:max-w-200 3xl:max-w-[1200px]": props.centered,
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
}}
>
<SessionTurn

View File

@@ -31,7 +31,7 @@ export function SessionPromptDock(props: {
<div
classList={{
"w-full px-4 pointer-events-auto": true,
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered,
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
}}
>
<Show when={props.questionRequest()} keyed>

View File

@@ -351,8 +351,8 @@ export const dict = {
"changelog.empty":
"\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0623\u064a \u0625\u062f\u062e\u0627\u0644\u0627\u062a \u0641\u064a \u0633\u062c\u0644 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a.",
"changelog.viewJson": "\u0639\u0631\u0636 JSON",
"workspace.nav.zen": "زين",
"workspace.nav.apiKeys": "API المفاتيح",
"workspace.nav.zen": "Zen",
"workspace.nav.apiKeys": "مفاتيح API",
"workspace.nav.members": "أعضاء",
"workspace.nav.billing": "الفواتير",
"workspace.nav.settings": "إعدادات",
@@ -365,14 +365,14 @@ export const dict = {
"workspace.newUser.feature.quality.title": "أعلى جودة",
"workspace.newUser.feature.quality.body":
"الوصول إلى النماذج التي تم تكوينها لتحقيق الأداء الأمثل - لا يوجد تخفيضات أو توجيه إلى موفري الخدمة الأرخص.",
"workspace.newUser.feature.lockin.title": "لا يوجد قفل",
"workspace.newUser.feature.lockin.title": "بدون احتجاز بمزوّد واحد",
"workspace.newUser.feature.lockin.body":
"استخدم Zen مع أي وكيل ترميز، واستمر في استخدام موفري الخدمات الآخرين مع opencode وقتما تشاء.",
"workspace.newUser.copyApiKey": "انسخ مفتاح API",
"workspace.newUser.copyKey": "نسخ المفتاح",
"workspace.newUser.copied": "منسوخ!",
"workspace.newUser.step.enableBilling": "تمكين الفوترة",
"workspace.newUser.step.login.before": "يجري",
"workspace.newUser.step.login.before": "شغّل",
"workspace.newUser.step.login.after": "وحدد opencode",
"workspace.newUser.step.pasteKey": "الصق مفتاح API الخاص بك",
"workspace.newUser.step.models.before": "ابدأ opencode ثم قم بالتشغيل",
@@ -390,7 +390,7 @@ export const dict = {
"workspace.providers.saving": "توفير...",
"workspace.providers.save": "يحفظ",
"workspace.providers.table.provider": "مزود",
"workspace.providers.table.apiKey": "API المفتاح",
"workspace.providers.table.apiKey": "مفتاح API",
"workspace.usage.title": "تاريخ الاستخدام",
"workspace.usage.subtitle": "استخدام وتكاليف API الأخيرة.",
"workspace.usage.empty": "قم بإجراء أول مكالمة API للبدء.",
@@ -398,25 +398,25 @@ export const dict = {
"workspace.usage.table.model": "نموذج",
"workspace.usage.table.input": "مدخل",
"workspace.usage.table.output": "الإخراج",
"workspace.usage.table.cost": "يكلف",
"workspace.usage.table.cost": "التكلفة",
"workspace.usage.breakdown.input": "مدخل",
"workspace.usage.breakdown.cacheRead": "قراءة ذاكرة التخزين المؤقت",
"workspace.usage.breakdown.cacheWrite": "كتابة ذاكرة التخزين المؤقت",
"workspace.usage.breakdown.output": "الإخراج",
"workspace.usage.breakdown.reasoning": "المنطق",
"workspace.usage.subscription": "الاشتراك (${{amount}})",
"workspace.cost.title": "يكلف",
"workspace.cost.title": "التكلفة",
"workspace.cost.subtitle": "تكاليف الاستخدام مقسمة حسب النموذج.",
"workspace.cost.allModels": "جميع الموديلات",
"workspace.cost.allModels": "جميع النماذج",
"workspace.cost.allKeys": "جميع المفاتيح",
"workspace.cost.deletedSuffix": "(محذوف)",
"workspace.cost.empty": "لا توجد بيانات استخدام متاحة للفترة المحددة.",
"workspace.cost.subscriptionShort": "الفرعية",
"workspace.keys.title": "API المفاتيح",
"workspace.cost.subscriptionShort": "اشتراك",
"workspace.keys.title": "مفاتيح API",
"workspace.keys.subtitle": "إدارة مفاتيح API الخاصة بك للوصول إلى خدمات opencode.",
"workspace.keys.create": "قم بإنشاء مفتاح API",
"workspace.keys.placeholder": "أدخل اسم المفتاح",
"workspace.keys.empty": "قم بإنشاء مفتاح opencode للبوابة API",
"workspace.keys.empty": "أنشئ مفتاح API لبوابة opencode",
"workspace.keys.table.name": "اسم",
"workspace.keys.table.key": "مفتاح",
"workspace.keys.table.createdBy": "تم الإنشاء بواسطة",
@@ -442,14 +442,14 @@ export const dict = {
"workspace.members.table.email": "بريد إلكتروني",
"workspace.members.table.role": "دور",
"workspace.members.table.monthLimit": "حد الشهر",
"workspace.members.role.admin": "مسؤل",
"workspace.members.role.admin": "مسؤول",
"workspace.members.role.adminDescription": "يمكن إدارة النماذج، والأعضاء، والفواتير",
"workspace.members.role.member": "عضو",
"workspace.members.role.memberDescription": "يمكنهم فقط إنشاء مفاتيح API لأنفسهم",
"workspace.settings.title": "إعدادات",
"workspace.settings.subtitle": "قم بتحديث اسم مساحة العمل الخاصة بك وتفضيلاتك.",
"workspace.settings.workspaceName": "اسم مساحة العمل",
"workspace.settings.defaultName": "تقصير",
"workspace.settings.defaultName": "الافتراضي",
"workspace.settings.updating": "جارٍ التحديث...",
"workspace.settings.save": "يحفظ",
"workspace.settings.edit": "يحرر",
@@ -461,37 +461,37 @@ export const dict = {
"workspace.billing.add": "أضف $",
"workspace.billing.enterAmount": "أدخل المبلغ",
"workspace.billing.loading": "تحميل...",
"workspace.billing.addAction": "يضيف",
"workspace.billing.addAction": "إضافة",
"workspace.billing.addBalance": "إضافة الرصيد",
"workspace.billing.linkedToStripe": "مرتبطة بالشريط",
"workspace.billing.manage": "يدير",
"workspace.billing.linkedToStripe": "مرتبط بـ Stripe",
"workspace.billing.manage": "إدارة",
"workspace.billing.enable": "تمكين الفوترة",
"workspace.monthlyLimit.title": "الحد الشهري",
"workspace.monthlyLimit.subtitle": "قم بتعيين حد الاستخدام الشهري لحسابك.",
"workspace.monthlyLimit.placeholder": "50",
"workspace.monthlyLimit.setting": لسة...",
"workspace.monthlyLimit.setting": ارٍ التعيين...",
"workspace.monthlyLimit.set": "تعيين",
"workspace.monthlyLimit.edit": "تحرير الحد",
"workspace.monthlyLimit.noLimit": "لم يتم تعيين حد الاستخدام.",
"workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي ل",
"workspace.monthlyLimit.currentUsage.beforeAmount": "هو $",
"workspace.reload.title": "إعادة التحميل التلقائي",
"workspace.reload.disabled.before": "إعادة التحميل التلقائي هو",
"workspace.reload.disabled.state": "عاجز",
"workspace.reload.disabled.after": "تمكين إعادة التحميل تلقائيًا عندما يكون الرصيد منخفضًا.",
"workspace.reload.enabled.before": "إعادة التحميل التلقائي هو",
"workspace.reload.title": "إعادة الشحن التلقائي",
"workspace.reload.disabled.before": "إعادة الشحن التلقائي",
"workspace.reload.disabled.state": "معطّل",
"workspace.reload.disabled.after": "فعّلها لإعادة شحن الرصيد تلقائيًا عندما يكون منخفضًا.",
"workspace.reload.enabled.before": "إعادة الشحن التلقائي",
"workspace.reload.enabled.state": "ممكّن",
"workspace.reload.enabled.middle": "سنقوم بإعادة التحميل",
"workspace.reload.enabled.middle": "سنعيد شحن رصيدك بمبلغ",
"workspace.reload.processingFee": "رسوم المعالجة",
"workspace.reload.enabled.after": "عندما يصل التوازن",
"workspace.reload.enabled.after": "عندما يصل الرصيد إلى",
"workspace.reload.edit": "يحرر",
"workspace.reload.enable": "يُمكَِن",
"workspace.reload.enableAutoReload": مكين إعادة التحميل التلقائي",
"workspace.reload.reloadAmount": "إعادة تحميل $",
"workspace.reload.enable": "تفعيل",
"workspace.reload.enableAutoReload": فعيل إعادة الشحن التلقائي",
"workspace.reload.reloadAmount": "مبلغ إعادة الشحن $",
"workspace.reload.whenBalanceReaches": "عندما يصل الرصيد إلى $",
"workspace.reload.saving": "توفير...",
"workspace.reload.save": "يحفظ",
"workspace.reload.failedAt": "فشلت عملية إعادة التحميل عند",
"workspace.reload.failedAt": "فشلت إعادة الشحن في",
"workspace.reload.reason": "سبب:",
"workspace.reload.updatePaymentMethod": "يرجى تحديث طريقة الدفع الخاصة بك والمحاولة مرة أخرى.",
"workspace.reload.retrying": "جارٍ إعادة المحاولة...",
@@ -500,11 +500,11 @@ export const dict = {
"workspace.payments.subtitle": "معاملات الدفع الأخيرة.",
"workspace.payments.table.date": "تاريخ",
"workspace.payments.table.paymentId": "معرف الدفع",
"workspace.payments.table.amount": "كمية",
"workspace.payments.table.amount": "المبلغ",
"workspace.payments.table.receipt": "إيصال",
"workspace.payments.type.credit": "ائتمان",
"workspace.payments.type.subscription": "الاشتراك",
"workspace.payments.view": "منظر",
"workspace.payments.view": "عرض",
"workspace.black.loading": "تحميل...",
"workspace.black.time.day": "يوم",
"workspace.black.time.days": "أيام",
@@ -521,8 +521,8 @@ export const dict = {
"workspace.black.subscription.resetsIn": "إعادة تعيين في",
"workspace.black.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
"workspace.black.waitlist.title": "قائمة الانتظار",
"workspace.black.waitlist.joined": "أنت على قائمة الانتظار للخطة السوداء {{plan}} دولار شهريًا OpenCode.",
"workspace.black.waitlist.ready": "نحن على استعداد لتسجيلك في خطة Black {{plan}} الشهرية OpenCode.",
"workspace.black.waitlist.joined": "أنت على قائمة الانتظار لخطة OpenCode Black بقيمة ${{plan}} شهريًا.",
"workspace.black.waitlist.ready": "نحن مستعدون لتسجيلك في خطة OpenCode Black بقيمة ${{plan}} شهريًا.",
"workspace.black.waitlist.leave": "ترك قائمة الانتظار",
"workspace.black.waitlist.leaving": "مغادرة...",
"workspace.black.waitlist.left": "غادر",

View File

@@ -294,18 +294,18 @@ export const dict = {
"workspace.home.billing.currentBalance": "Nuværende saldo",
"workspace.newUser.feature.tested.title": "Testede og verificerede modeller",
"workspace.newUser.feature.tested.body":
"Vi har benchmarket og testet modeller specifikt til kodningsmidler for at sikre den bedste ydeevne.",
"Vi har benchmarket og testet modeller specifikt til kodningsagenter for at sikre den bedste ydeevne.",
"workspace.newUser.feature.quality.title": "Højeste kvalitet",
"workspace.newUser.feature.quality.body":
"Få adgang til modeller konfigureret til optimal ydeevne - ingen nedgraderinger eller routing til billigere udbydere.",
"workspace.newUser.feature.lockin.title": "Ingen indlåsning",
"workspace.newUser.feature.lockin.body":
"Brug Zen med en hvilken som helst kodningsagent, og fortsæt med at bruge andre udbydere med opencode, når du vil.",
"workspace.newUser.copyApiKey": "Kopiér nøglen API",
"workspace.newUser.copyApiKey": "Kopiér API-nøgle",
"workspace.newUser.copyKey": "Kopier nøgle",
"workspace.newUser.copied": "Kopieret!",
"workspace.newUser.step.enableBilling": "Aktiver fakturering",
"workspace.newUser.step.login.before": "Løbe",
"workspace.newUser.step.login.before": "Kør",
"workspace.newUser.step.login.after": "og vælg opencode",
"workspace.newUser.step.pasteKey": "Indsæt din API nøgle",
"workspace.newUser.step.models.before": "Start opencode og kør",
@@ -316,12 +316,12 @@ export const dict = {
"workspace.models.table.enabled": "Aktiveret",
"workspace.providers.title": "Medbring din egen nøgle",
"workspace.providers.subtitle": "Konfigurer dine egne API nøgler fra AI-udbydere.",
"workspace.providers.placeholder": "Indtast nøglen {{provider}} API ({{prefix}}...)",
"workspace.providers.placeholder": "Indtast {{provider}} API-nøgle ({{prefix}}...)",
"workspace.providers.configure": "Konfigurer",
"workspace.providers.edit": "Redigere",
"workspace.providers.edit": "Rediger",
"workspace.providers.delete": "Slet",
"workspace.providers.saving": "Gemmer...",
"workspace.providers.save": "Spare",
"workspace.providers.save": "Gem",
"workspace.providers.table.provider": "Udbyder",
"workspace.providers.table.apiKey": "API Nøgle",
"workspace.usage.title": "Brugshistorik",
@@ -330,15 +330,15 @@ export const dict = {
"workspace.usage.table.date": "Dato",
"workspace.usage.table.model": "Model",
"workspace.usage.table.input": "Input",
"workspace.usage.table.output": "Produktion",
"workspace.usage.table.cost": "Koste",
"workspace.usage.table.output": "Output",
"workspace.usage.table.cost": "Omkostning",
"workspace.usage.breakdown.input": "Input",
"workspace.usage.breakdown.cacheRead": "Cache læst",
"workspace.usage.breakdown.cacheWrite": "Cache skriv",
"workspace.usage.breakdown.output": "Produktion",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Ræsonnement",
"workspace.usage.subscription": "abonnement (${{amount}})",
"workspace.cost.title": "Koste",
"workspace.cost.title": "Omkostninger",
"workspace.cost.subtitle": "Brugsomkostninger opdelt efter model.",
"workspace.cost.allModels": "Alle modeller",
"workspace.cost.allKeys": "Alle nøgler",
@@ -354,7 +354,7 @@ export const dict = {
"workspace.keys.table.key": "Nøgle",
"workspace.keys.table.createdBy": "Skabt af",
"workspace.keys.table.lastUsed": "Sidst brugt",
"workspace.keys.copyApiKey": "Kopiér nøglen API",
"workspace.keys.copyApiKey": "Kopiér API-nøgle",
"workspace.keys.delete": "Slet",
"workspace.members.title": "Medlemmer",
"workspace.members.subtitle": "Administrer arbejdsområdemedlemmer og deres tilladelser.",
@@ -368,10 +368,10 @@ export const dict = {
"workspace.members.noLimit": "Ingen grænse",
"workspace.members.noLimitLowercase": "ingen grænse",
"workspace.members.invited": "inviteret",
"workspace.members.edit": "Redigere",
"workspace.members.edit": "Rediger",
"workspace.members.delete": "Slet",
"workspace.members.saving": "Gemmer...",
"workspace.members.save": "Spare",
"workspace.members.save": "Gem",
"workspace.members.table.email": "E-mail",
"workspace.members.table.role": "Rolle",
"workspace.members.table.monthLimit": "Månedsgrænse",
@@ -382,10 +382,10 @@ export const dict = {
"workspace.settings.title": "Indstillinger",
"workspace.settings.subtitle": "Opdater dit arbejdsområdes navn og præferencer.",
"workspace.settings.workspaceName": "Arbejdsområdets navn",
"workspace.settings.defaultName": "Misligholdelse",
"workspace.settings.defaultName": "Standard",
"workspace.settings.updating": "Opdaterer...",
"workspace.settings.save": "Spare",
"workspace.settings.edit": "Redigere",
"workspace.settings.save": "Gem",
"workspace.settings.edit": "Rediger",
"workspace.billing.title": "Fakturering",
"workspace.billing.subtitle.beforeLink": "Administrer betalingsmetoder.",
"workspace.billing.contactUs": "Kontakt os",
@@ -394,10 +394,10 @@ export const dict = {
"workspace.billing.add": "Tilføj $",
"workspace.billing.enterAmount": "Indtast beløb",
"workspace.billing.loading": "Indlæser...",
"workspace.billing.addAction": "Tilføje",
"workspace.billing.addAction": "Tilføj",
"workspace.billing.addBalance": "Tilføj balance",
"workspace.billing.linkedToStripe": "Forbundet til Stripe",
"workspace.billing.manage": "Styre",
"workspace.billing.manage": "Administrer",
"workspace.billing.enable": "Aktiver fakturering",
"workspace.monthlyLimit.title": "Månedlig grænse",
"workspace.monthlyLimit.subtitle": "Indstil en månedlig forbrugsgrænse for din konto.",
@@ -408,23 +408,23 @@ export const dict = {
"workspace.monthlyLimit.noLimit": "Ingen forbrugsgrænse angivet.",
"workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
"workspace.reload.title": "Automatisk genindlæsning",
"workspace.reload.disabled.before": "Automatisk genindlæsning er",
"workspace.reload.disabled.state": "handicappet",
"workspace.reload.disabled.after": "Aktiver for automatisk at genindlæse, når balancen er lav.",
"workspace.reload.enabled.before": "Automatisk genindlæsning er",
"workspace.reload.title": "Automatisk genopfyldning",
"workspace.reload.disabled.before": "Automatisk genopfyldning er",
"workspace.reload.disabled.state": "deaktiveret",
"workspace.reload.disabled.after": "Aktiver for automatisk at genopfylde, når saldoen er lav.",
"workspace.reload.enabled.before": "Automatisk genopfyldning er",
"workspace.reload.enabled.state": "aktiveret",
"workspace.reload.enabled.middle": "Vi genindlæser",
"workspace.reload.enabled.middle": "Vi genopfylder",
"workspace.reload.processingFee": "ekspeditionsgebyr",
"workspace.reload.enabled.after": "når balancen er nået",
"workspace.reload.edit": "Redigere",
"workspace.reload.edit": "Rediger",
"workspace.reload.enable": "Aktiver",
"workspace.reload.enableAutoReload": "Aktiver automatisk genindlæsning",
"workspace.reload.reloadAmount": "Genindlæs $",
"workspace.reload.enableAutoReload": "Aktiver automatisk genopfyldning",
"workspace.reload.reloadAmount": "Genopfyld $",
"workspace.reload.whenBalanceReaches": "Når saldoen når $",
"workspace.reload.saving": "Gemmer...",
"workspace.reload.save": "Spare",
"workspace.reload.failedAt": "Genindlæsning mislykkedes kl",
"workspace.reload.save": "Gem",
"workspace.reload.failedAt": "Genopfyldning mislykkedes kl",
"workspace.reload.reason": "Årsag:",
"workspace.reload.updatePaymentMethod": "Opdater din betalingsmetode, og prøv igen.",
"workspace.reload.retrying": "Prøver igen...",
@@ -434,10 +434,10 @@ export const dict = {
"workspace.payments.table.date": "Dato",
"workspace.payments.table.paymentId": "Betalings-id",
"workspace.payments.table.amount": "Beløb",
"workspace.payments.table.receipt": "Modtagelse",
"workspace.payments.table.receipt": "Kvittering",
"workspace.payments.type.credit": "kredit",
"workspace.payments.type.subscription": "abonnement",
"workspace.payments.view": "Udsigt",
"workspace.payments.view": "Vis",
"workspace.black.loading": "Indlæser...",
"workspace.black.time.day": "dag",
"workspace.black.time.days": "dage",
@@ -458,8 +458,8 @@ export const dict = {
"workspace.black.waitlist.ready": "Vi er klar til at tilmelde dig ${{plan}} per måned OpenCode Black plan.",
"workspace.black.waitlist.leave": "Forlad venteliste",
"workspace.black.waitlist.leaving": "Forlader...",
"workspace.black.waitlist.left": "Venstre",
"workspace.black.waitlist.enroll": "Indskrive",
"workspace.black.waitlist.left": "Forladt",
"workspace.black.waitlist.enroll": "Tilmeld",
"workspace.black.waitlist.enrolling": "Tilmelder...",
"workspace.black.waitlist.enrolled": "Tilmeldt",
"workspace.black.waitlist.enrollNote":

View File

@@ -306,27 +306,27 @@ export const dict = {
"workspace.newUser.feature.lockin.title": "Kein Lock-in",
"workspace.newUser.feature.lockin.body":
"Verwenden Sie Zen mit einem beliebigen Codierungsagenten und nutzen Sie weiterhin andere Anbieter mit opencode, wann immer Sie möchten.",
"workspace.newUser.copyApiKey": "Kopieren Sie den Schlüssel API",
"workspace.newUser.copyApiKey": "API-Schlüssel kopieren",
"workspace.newUser.copyKey": "Schlüssel kopieren",
"workspace.newUser.copied": "Kopiert!",
"workspace.newUser.step.enableBilling": "Abrechnung aktivieren",
"workspace.newUser.step.login.before": "Laufen",
"workspace.newUser.step.login.before": "Führe",
"workspace.newUser.step.login.after": "und wählen Sie opencode",
"workspace.newUser.step.pasteKey": "Fügen Sie Ihren API-Schlüssel ein",
"workspace.newUser.step.models.before": "Starten Sie opencode und führen Sie es aus",
"workspace.newUser.step.models.before": "Starte opencode und führe",
"workspace.newUser.step.models.after": "um ein Modell auszuwählen",
"workspace.models.title": "Modelle",
"workspace.models.subtitle.beforeLink":
"Verwalten Sie, auf welche Modelle Arbeitsbereichsmitglieder zugreifen können.",
"workspace.models.table.model": "Modell",
"workspace.models.table.enabled": "Ermöglicht",
"workspace.models.table.enabled": "Aktiviert",
"workspace.providers.title": "Bringen Sie Ihren eigenen Schlüssel mit",
"workspace.providers.subtitle": "Konfigurieren Sie Ihre eigenen API-Schlüssel von KI-Anbietern.",
"workspace.providers.placeholder": "Geben Sie den Schlüssel {{provider}} API ein ({{prefix}}...)",
"workspace.providers.configure": "Konfigurieren",
"workspace.providers.edit": "Bearbeiten",
"workspace.providers.delete": "Löschen",
"workspace.providers.saving": "Sparen...",
"workspace.providers.saving": "Wird gespeichert...",
"workspace.providers.save": "Speichern",
"workspace.providers.table.provider": "Anbieter",
"workspace.providers.table.apiKey": "API-Schlüssel",
@@ -335,14 +335,14 @@ export const dict = {
"workspace.usage.empty": "Machen Sie Ihren ersten API-Aufruf, um loszulegen.",
"workspace.usage.table.date": "Datum",
"workspace.usage.table.model": "Modell",
"workspace.usage.table.input": "Eingang",
"workspace.usage.table.output": "Ausgabe",
"workspace.usage.table.input": "Input",
"workspace.usage.table.output": "Output",
"workspace.usage.table.cost": "Kosten",
"workspace.usage.breakdown.input": "Eingang",
"workspace.usage.breakdown.input": "Input",
"workspace.usage.breakdown.cacheRead": "Cache-Lesen",
"workspace.usage.breakdown.cacheWrite": "Cache-Schreiben",
"workspace.usage.breakdown.output": "Ausgabe",
"workspace.usage.breakdown.reasoning": "Argumentation",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Reasoning",
"workspace.usage.subscription": "Abonnement (${{amount}})",
"workspace.cost.title": "Kosten",
"workspace.cost.subtitle": "Nutzungskosten aufgeschlüsselt nach Modell.",
@@ -360,12 +360,12 @@ export const dict = {
"workspace.keys.table.key": "Schlüssel",
"workspace.keys.table.createdBy": "Erstellt von",
"workspace.keys.table.lastUsed": "Zuletzt verwendet",
"workspace.keys.copyApiKey": "Kopieren Sie den Schlüssel API",
"workspace.keys.copyApiKey": "API-Schlüssel kopieren",
"workspace.keys.delete": "Löschen",
"workspace.members.title": "Mitglieder",
"workspace.members.subtitle": "Verwalten Sie Arbeitsbereichsmitglieder und ihre Berechtigungen.",
"workspace.members.invite": "Mitglied einladen",
"workspace.members.inviting": "Einladend...",
"workspace.members.inviting": "Wird eingeladen...",
"workspace.members.beta.beforeLink": "Während der Betaversion sind Arbeitsbereiche für Teams kostenlos.",
"workspace.members.form.invitee": "Eingeladen",
"workspace.members.form.emailPlaceholder": "Geben Sie Ihre E-Mail-Adresse ein",
@@ -376,7 +376,7 @@ export const dict = {
"workspace.members.invited": "eingeladen",
"workspace.members.edit": "Bearbeiten",
"workspace.members.delete": "Löschen",
"workspace.members.saving": "Sparen...",
"workspace.members.saving": "Wird gespeichert...",
"workspace.members.save": "Speichern",
"workspace.members.table.email": "E-Mail",
"workspace.members.table.role": "Rolle",
@@ -408,30 +408,30 @@ export const dict = {
"workspace.monthlyLimit.title": "Monatliches Limit",
"workspace.monthlyLimit.subtitle": "Legen Sie ein monatliches Nutzungslimit für Ihr Konto fest.",
"workspace.monthlyLimit.placeholder": "50",
"workspace.monthlyLimit.setting": "Einstellung...",
"workspace.monthlyLimit.set": "Satz",
"workspace.monthlyLimit.setting": "Wird gesetzt...",
"workspace.monthlyLimit.set": "Festlegen",
"workspace.monthlyLimit.edit": "Limit bearbeiten",
"workspace.monthlyLimit.noLimit": "Kein Nutzungslimit festgelegt.",
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
"workspace.monthlyLimit.currentUsage.beforeAmount": "ist $",
"workspace.reload.title": "Automatisches Neuladen",
"workspace.reload.disabled.before": "Automatisches Nachladen ist",
"workspace.reload.title": "Automatische Aufladung",
"workspace.reload.disabled.before": "Automatische Aufladung ist",
"workspace.reload.disabled.state": "deaktiviert",
"workspace.reload.disabled.after":
"Aktivieren Sie diese Option, um das Guthaben automatisch neu zu laden, wenn das Guthaben niedrig ist.",
"workspace.reload.enabled.before": "Automatisches Nachladen ist",
"workspace.reload.enabled.state": "ermöglicht",
"workspace.reload.enabled.middle": "Wir laden nach",
"Aktivieren Sie diese Option, damit bei niedrigem Kontostand automatisch aufgeladen wird.",
"workspace.reload.enabled.before": "Automatische Aufladung ist",
"workspace.reload.enabled.state": "aktiviert",
"workspace.reload.enabled.middle": "Wir laden auf",
"workspace.reload.processingFee": "Bearbeitungsgebühr",
"workspace.reload.enabled.after": "wenn das Gleichgewicht erreicht ist",
"workspace.reload.enabled.after": "sobald der Kontostand",
"workspace.reload.edit": "Bearbeiten",
"workspace.reload.enable": "Aktivieren",
"workspace.reload.enableAutoReload": "Aktivieren Sie das automatische Neuladen",
"workspace.reload.reloadAmount": "$ neu laden",
"workspace.reload.whenBalanceReaches": "Wenn der Saldo $ erreicht",
"workspace.reload.saving": "Sparen...",
"workspace.reload.enableAutoReload": "Automatische Aufladung aktivieren",
"workspace.reload.reloadAmount": "Aufladebetrag $",
"workspace.reload.whenBalanceReaches": "Wenn der Kontostand $ erreicht",
"workspace.reload.saving": "Wird gespeichert...",
"workspace.reload.save": "Speichern",
"workspace.reload.failedAt": "Neuladen fehlgeschlagen bei",
"workspace.reload.failedAt": "Aufladung fehlgeschlagen am",
"workspace.reload.reason": "Grund:",
"workspace.reload.updatePaymentMethod": "Bitte aktualisieren Sie Ihre Zahlungsmethode und versuchen Sie es erneut.",
"workspace.reload.retrying": "Erneuter Versuch...",
@@ -440,11 +440,11 @@ export const dict = {
"workspace.payments.subtitle": "Letzte Zahlungsvorgänge.",
"workspace.payments.table.date": "Datum",
"workspace.payments.table.paymentId": "Zahlungs-ID",
"workspace.payments.table.amount": "Menge",
"workspace.payments.table.amount": "Betrag",
"workspace.payments.table.receipt": "Quittung",
"workspace.payments.type.credit": "Kredit",
"workspace.payments.type.subscription": "Abonnement",
"workspace.payments.view": "Sicht",
"workspace.payments.view": "Anzeigen",
"workspace.black.loading": "Laden...",
"workspace.black.time.day": "Tag",
"workspace.black.time.days": "Tage",
@@ -454,21 +454,21 @@ export const dict = {
"workspace.black.time.minutes": "Minuten",
"workspace.black.time.fewSeconds": "ein paar Sekunden",
"workspace.black.subscription.title": "Abonnement",
"workspace.black.subscription.message": "Sie haben OpenCode Black für {{plan}} pro Monat abonniert.",
"workspace.black.subscription.message": "Sie haben OpenCode Black für ${{plan}} pro Monat abonniert.",
"workspace.black.subscription.manage": "Abonnement verwalten",
"workspace.black.subscription.rollingUsage": "5-stündige Nutzung",
"workspace.black.subscription.weeklyUsage": "Wöchentliche Nutzung",
"workspace.black.subscription.resetsIn": "Wird zurückgesetzt",
"workspace.black.subscription.resetsIn": "Zurückgesetzt in",
"workspace.black.subscription.useBalance":
"Nutzen Sie Ihr verfügbares Guthaben, nachdem Sie die Nutzungslimits erreicht haben",
"workspace.black.waitlist.title": "Warteliste",
"workspace.black.waitlist.joined":
"Sie stehen auf der Warteliste für den Black-Plan im Wert von ${{plan}} pro Monat OpenCode.",
"Sie stehen auf der Warteliste für den OpenCode Black Tarif für ${{plan}} pro Monat.",
"workspace.black.waitlist.ready":
"Wir sind bereit, Sie für den Black-Plan im Wert von ${{plan}} pro Monat OpenCode anzumelden.",
"Wir können Sie jetzt in den OpenCode Black Tarif für ${{plan}} pro Monat aufnehmen.",
"workspace.black.waitlist.leave": "Warteliste verlassen",
"workspace.black.waitlist.leaving": "Verlassen...",
"workspace.black.waitlist.left": "Links",
"workspace.black.waitlist.left": "Verlassen",
"workspace.black.waitlist.enroll": "Einschreiben",
"workspace.black.waitlist.enrolling": "Anmeldung...",
"workspace.black.waitlist.enrolled": "Eingeschrieben",

View File

@@ -284,8 +284,8 @@ export const dict = {
"changelog.hero.subtitle": "Nuovi aggiornamenti e miglioramenti per OpenCode",
"changelog.empty": "Nessuna voce di changelog trovata.",
"changelog.viewJson": "Visualizza JSON",
"workspace.nav.zen": "zen",
"workspace.nav.apiKeys": "API Chiavi",
"workspace.nav.zen": "Zen",
"workspace.nav.apiKeys": "Chiavi API",
"workspace.nav.members": "Membri",
"workspace.nav.billing": "Fatturazione",
"workspace.nav.settings": "Impostazioni",
@@ -299,14 +299,14 @@ export const dict = {
"workspace.newUser.feature.quality.title": "Massima qualità",
"workspace.newUser.feature.quality.body":
"Modelli di accesso configurati per prestazioni ottimali: senza downgrade o instradamento verso fornitori più economici.",
"workspace.newUser.feature.lockin.title": "Nessun blocco",
"workspace.newUser.feature.lockin.title": "Nessun lock-in",
"workspace.newUser.feature.lockin.body":
"Utilizza Zen con qualsiasi agente di codifica e continua a utilizzare altri provider con opencode ogni volta che vuoi.",
"workspace.newUser.copyApiKey": "Copia la chiave API",
"workspace.newUser.copyKey": "Copia chiave",
"workspace.newUser.copied": "Copiato!",
"workspace.newUser.step.enableBilling": "Abilita fatturazione",
"workspace.newUser.step.login.before": "Correre",
"workspace.newUser.step.login.before": "Esegui",
"workspace.newUser.step.login.after": "e seleziona opencode",
"workspace.newUser.step.pasteKey": "Incolla la tua chiave API",
"workspace.newUser.step.models.before": "Avvia opencode ed esegui",
@@ -315,16 +315,16 @@ export const dict = {
"workspace.models.subtitle.beforeLink": "Gestire i modelli a cui possono accedere i membri dell'area di lavoro.",
"workspace.models.table.model": "Modello",
"workspace.models.table.enabled": "Abilitato",
"workspace.providers.title": "Porta la tua chiave",
"workspace.providers.title": "Bring Your Own Key (BYOK)",
"workspace.providers.subtitle": "Configura le tue chiavi API dai fornitori di intelligenza artificiale.",
"workspace.providers.placeholder": "Inserisci la chiave {{provider}} API ({{prefix}}...)",
"workspace.providers.configure": "Configura",
"workspace.providers.edit": "Modificare",
"workspace.providers.delete": "Eliminare",
"workspace.providers.saving": "Risparmio...",
"workspace.providers.saving": "Salvataggio in corso...",
"workspace.providers.save": "Salva",
"workspace.providers.table.provider": "Fornitore",
"workspace.providers.table.apiKey": "API Chiave",
"workspace.providers.table.apiKey": "Chiave API",
"workspace.usage.title": "Cronologia dell'utilizzo",
"workspace.usage.subtitle": "Utilizzo e costi recenti di API.",
"workspace.usage.empty": "Effettua la tua prima chiamata API per iniziare.",
@@ -346,7 +346,7 @@ export const dict = {
"workspace.cost.deletedSuffix": "(eliminato)",
"workspace.cost.empty": "Nessun dato di utilizzo disponibile per il periodo selezionato.",
"workspace.cost.subscriptionShort": "sub",
"workspace.keys.title": "API Chiavi",
"workspace.keys.title": "Chiavi API",
"workspace.keys.subtitle": "Gestisci le tue chiavi API per accedere ai servizi opencode.",
"workspace.keys.create": "Crea chiave API",
"workspace.keys.placeholder": "Inserisci il nome della chiave",
@@ -360,7 +360,7 @@ export const dict = {
"workspace.members.title": "Membri",
"workspace.members.subtitle": "Gestire i membri dell'area di lavoro e le relative autorizzazioni.",
"workspace.members.invite": "Invita membro",
"workspace.members.inviting": "Invitante...",
"workspace.members.inviting": "Invito in corso...",
"workspace.members.beta.beforeLink": "Gli spazi di lavoro sono gratuiti per i team durante la beta.",
"workspace.members.form.invitee": "Invitato",
"workspace.members.form.emailPlaceholder": "Inserisci l'e-mail",
@@ -371,12 +371,12 @@ export const dict = {
"workspace.members.invited": "invitato",
"workspace.members.edit": "Modificare",
"workspace.members.delete": "Eliminare",
"workspace.members.saving": "Risparmio...",
"workspace.members.saving": "Salvataggio in corso...",
"workspace.members.save": "Salva",
"workspace.members.table.email": "E-mail",
"workspace.members.table.role": "Ruolo",
"workspace.members.table.monthLimit": "Limite mensile",
"workspace.members.role.admin": "Ammin",
"workspace.members.role.admin": "Admin",
"workspace.members.role.adminDescription": "Può gestire modelli, membri e fatturazione",
"workspace.members.role.member": "Membro",
"workspace.members.role.memberDescription": "Possono generare chiavi API solo per se stessi",
@@ -388,42 +388,42 @@ export const dict = {
"workspace.settings.save": "Salva",
"workspace.settings.edit": "Modificare",
"workspace.billing.title": "Fatturazione",
"workspace.billing.subtitle.beforeLink": "Gestire i metodi di pagamento.",
"workspace.billing.subtitle.beforeLink": "Gestisci i metodi di pagamento.",
"workspace.billing.contactUs": "Contattaci",
"workspace.billing.subtitle.afterLink": "se hai qualche domanda",
"workspace.billing.currentBalance": "Saldo attuale",
"workspace.billing.add": "Aggiungi $",
"workspace.billing.enterAmount": "Inserisci l'importo",
"workspace.billing.loading": "Caricamento...",
"workspace.billing.addAction": "Aggiungere",
"workspace.billing.addAction": "Aggiungi",
"workspace.billing.addBalance": "Aggiungi saldo",
"workspace.billing.linkedToStripe": "Collegato a Stripe",
"workspace.billing.manage": "Maneggio",
"workspace.billing.manage": "Gestisci",
"workspace.billing.enable": "Abilita fatturazione",
"workspace.monthlyLimit.title": "Limite mensile",
"workspace.monthlyLimit.subtitle": "Imposta un limite di utilizzo mensile per il tuo account.",
"workspace.monthlyLimit.placeholder": "50",
"workspace.monthlyLimit.setting": "Collocamento...",
"workspace.monthlyLimit.setting": "Impostazione in corso...",
"workspace.monthlyLimit.set": "Impostato",
"workspace.monthlyLimit.edit": "Modifica limite",
"workspace.monthlyLimit.noLimit": "Nessun limite di utilizzo impostato.",
"workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
"workspace.monthlyLimit.currentUsage.beforeAmount": "è $",
"workspace.reload.title": "Ricarica automatica",
"workspace.reload.disabled.before": "La ricarica automatica lo è",
"workspace.reload.disabled.before": "La ricarica automatica è",
"workspace.reload.disabled.state": "disabilitato",
"workspace.reload.disabled.after": "Abilita la ricarica automatica quando il saldo è basso.",
"workspace.reload.enabled.before": "La ricarica automatica lo è",
"workspace.reload.enabled.before": "La ricarica automatica è",
"workspace.reload.enabled.state": "abilitato",
"workspace.reload.enabled.middle": "Ricaricheremo",
"workspace.reload.processingFee": "tassa di elaborazione",
"workspace.reload.enabled.after": "quando l'equilibrio raggiunge",
"workspace.reload.enabled.after": "quando il saldo raggiunge",
"workspace.reload.edit": "Modificare",
"workspace.reload.enable": "Abilitare",
"workspace.reload.enableAutoReload": "Abilita ricarica automatica",
"workspace.reload.reloadAmount": "Ricarica $",
"workspace.reload.whenBalanceReaches": "Quando il saldo raggiunge $",
"workspace.reload.saving": "Risparmio...",
"workspace.reload.saving": "Salvataggio in corso...",
"workspace.reload.save": "Salva",
"workspace.reload.failedAt": "Ricarica non riuscita a",
"workspace.reload.reason": "Motivo:",
@@ -434,11 +434,11 @@ export const dict = {
"workspace.payments.subtitle": "Transazioni di pagamento recenti.",
"workspace.payments.table.date": "Data",
"workspace.payments.table.paymentId": "ID pagamento",
"workspace.payments.table.amount": "Quantità",
"workspace.payments.table.amount": "Importo",
"workspace.payments.table.receipt": "Ricevuta",
"workspace.payments.type.credit": "credito",
"workspace.payments.type.subscription": "sottoscrizione",
"workspace.payments.view": "Visualizzazione",
"workspace.payments.view": "Visualizza",
"workspace.black.loading": "Caricamento...",
"workspace.black.time.day": "giorno",
"workspace.black.time.days": "giorni",
@@ -452,14 +452,14 @@ export const dict = {
"workspace.black.subscription.manage": "Gestisci abbonamento",
"workspace.black.subscription.rollingUsage": "Utilizzo di 5 ore",
"workspace.black.subscription.weeklyUsage": "Utilizzo settimanale",
"workspace.black.subscription.resetsIn": "Si reimposta",
"workspace.black.subscription.resetsIn": "Si reimposta tra",
"workspace.black.subscription.useBalance": "Utilizza il saldo disponibile dopo aver raggiunto i limiti di utilizzo",
"workspace.black.waitlist.title": "Lista d'attesa",
"workspace.black.waitlist.joined": "Sei in lista d'attesa per il piano nero ${{plan}} al mese OpenCode.",
"workspace.black.waitlist.joined": "Sei in lista d'attesa per il piano OpenCode Black da ${{plan}} al mese.",
"workspace.black.waitlist.ready": "Siamo pronti per iscriverti al piano OpenCode Black da ${{plan}} al mese.",
"workspace.black.waitlist.leave": "Lascia la lista d'attesa",
"workspace.black.waitlist.leaving": "In partenza...",
"workspace.black.waitlist.left": "Sinistra",
"workspace.black.waitlist.leaving": "Uscita in corso...",
"workspace.black.waitlist.left": "Uscito dalla lista d'attesa",
"workspace.black.waitlist.enroll": "Iscriversi",
"workspace.black.waitlist.enrolling": "Iscrizione...",
"workspace.black.waitlist.enrolled": "Iscritto",

View File

@@ -293,9 +293,9 @@ export const dict = {
"changelog.hero.subtitle": "OpenCode \u7684\u65b0\u66f4\u65b0\u4e0e\u6539\u8fdb",
"changelog.empty": "\u672a\u627e\u5230\u66f4\u65b0\u65e5\u5fd7\u6761\u76ee\u3002",
"changelog.viewJson": "\u67e5\u770b JSON",
"workspace.nav.zen": "",
"workspace.nav.zen": "Zen",
"workspace.nav.apiKeys": "API 键",
"workspace.nav.members": "员",
"workspace.nav.members": "员",
"workspace.nav.billing": "计费",
"workspace.nav.settings": "设置",
"workspace.home.banner.beforeLink": "编码代理的可靠优化模型。",
@@ -310,26 +310,26 @@ export const dict = {
"workspace.newUser.feature.lockin.body":
"将 Zen 与任何编码代理结合使用,并在需要时继续将其他提供程序与 opencode 结合使用。",
"workspace.newUser.copyApiKey": "复制 API 密钥",
"workspace.newUser.copyKey": "复制钥",
"workspace.newUser.copied": "复制",
"workspace.newUser.copyKey": "复制钥",
"workspace.newUser.copied": "复制!",
"workspace.newUser.step.enableBilling": "启用计费",
"workspace.newUser.step.login.before": "跑步",
"workspace.newUser.step.login.before": "运行",
"workspace.newUser.step.login.after": "并选择 opencode",
"workspace.newUser.step.pasteKey": "粘贴您的 API 密钥",
"workspace.newUser.step.models.before": "启动 opencode 并运行",
"workspace.newUser.step.models.after": "选择型",
"workspace.models.title": "型",
"workspace.newUser.step.models.after": "选择型",
"workspace.models.title": "型",
"workspace.models.subtitle.beforeLink": "管理工作区成员可以访问哪些模型。",
"workspace.models.table.model": "模型",
"workspace.models.table.enabled": "启用",
"workspace.providers.title": "带上你自己的钥匙",
"workspace.providers.title": "自带密钥",
"workspace.providers.subtitle": "从 AI 提供商处配置您自己的 API 密钥。",
"workspace.providers.placeholder": "输入 {{provider}} API 密钥({{prefix}}...",
"workspace.providers.configure": "配置",
"workspace.providers.edit": "编辑",
"workspace.providers.delete": "删除",
"workspace.providers.saving": "保存...",
"workspace.providers.save": "节省",
"workspace.providers.save": "保存",
"workspace.providers.table.provider": "提供者",
"workspace.providers.table.apiKey": "API 密钥",
"workspace.usage.title": "使用历史",
@@ -348,25 +348,25 @@ export const dict = {
"workspace.usage.subscription": "订阅 (${{amount}})",
"workspace.cost.title": "成本",
"workspace.cost.subtitle": "按型号细分的使用成本。",
"workspace.cost.allModels": "所有型",
"workspace.cost.allKeys": "所有按键",
"workspace.cost.allModels": "所有型",
"workspace.cost.allKeys": "所有密钥",
"workspace.cost.deletedSuffix": "(已删除)",
"workspace.cost.empty": "所选期间没有可用的使用数据。",
"workspace.cost.subscriptionShort": "",
"workspace.cost.subscriptionShort": "",
"workspace.keys.title": "API 键",
"workspace.keys.subtitle": "管理您的 API 密钥以访问 opencode 服务。",
"workspace.keys.create": "创建 API 密钥",
"workspace.keys.placeholder": "输入按键名称",
"workspace.keys.placeholder": "输入密钥名称",
"workspace.keys.empty": "创建 opencode 网关 API 密钥",
"workspace.keys.table.name": "名",
"workspace.keys.table.key": "钥",
"workspace.keys.table.name": "名",
"workspace.keys.table.key": "钥",
"workspace.keys.table.createdBy": "创建者",
"workspace.keys.table.lastUsed": "最后使用",
"workspace.keys.copyApiKey": "复制 API 密钥",
"workspace.keys.delete": "删除",
"workspace.members.title": "员",
"workspace.members.title": "员",
"workspace.members.subtitle": "管理工作区成员及其权限。",
"workspace.members.invite": "邀请员",
"workspace.members.invite": "邀请员",
"workspace.members.inviting": "邀请...",
"workspace.members.beta.beforeLink": "测试期间,工作空间对团队免费。",
"workspace.members.form.invitee": "受邀者",
@@ -379,11 +379,11 @@ export const dict = {
"workspace.members.edit": "编辑",
"workspace.members.delete": "删除",
"workspace.members.saving": "保存...",
"workspace.members.save": "节省",
"workspace.members.save": "保存",
"workspace.members.table.email": "电子邮件",
"workspace.members.table.role": "角色",
"workspace.members.table.monthLimit": "月份限制",
"workspace.members.role.admin": "行政",
"workspace.members.table.monthLimit": "月限额",
"workspace.members.role.admin": "管理员",
"workspace.members.role.adminDescription": "可以管理模型、成员和计费",
"workspace.members.role.member": "成员",
"workspace.members.role.memberDescription": "只能为自己生成 API 密钥",
@@ -392,7 +392,7 @@ export const dict = {
"workspace.settings.workspaceName": "工作区名称",
"workspace.settings.defaultName": "默认",
"workspace.settings.updating": "更新中...",
"workspace.settings.save": "节省",
"workspace.settings.save": "保存",
"workspace.settings.edit": "编辑",
"workspace.billing.title": "计费",
"workspace.billing.subtitle.beforeLink": "管理付款方式。",
@@ -404,35 +404,35 @@ export const dict = {
"workspace.billing.loading": "加载中...",
"workspace.billing.addAction": "添加",
"workspace.billing.addBalance": "添加余额",
"workspace.billing.linkedToStripe": "链接到条纹",
"workspace.billing.linkedToStripe": "已绑定 Stripe",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "启用计费",
"workspace.monthlyLimit.title": "每月限额",
"workspace.monthlyLimit.subtitle": "为您的帐户设置每月使用限额。",
"workspace.monthlyLimit.placeholder": "50",
"workspace.monthlyLimit.setting": "环境...",
"workspace.monthlyLimit.set": "",
"workspace.monthlyLimit.setting": "设置中...",
"workspace.monthlyLimit.set": "设置",
"workspace.monthlyLimit.edit": "编辑限制",
"workspace.monthlyLimit.noLimit": "没有设置使用限制。",
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前使用情况为",
"workspace.monthlyLimit.currentUsage.beforeAmount": " $",
"workspace.reload.title": "自动重新加载",
"workspace.reload.disabled.before": "自动重新加载是",
"workspace.reload.disabled.state": "残疾人",
"workspace.reload.disabled.after": "启用余额不足时自动充值。",
"workspace.reload.enabled.before": "自动重新加载是",
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $",
"workspace.reload.title": "自动充值",
"workspace.reload.disabled.before": "自动充值已",
"workspace.reload.disabled.state": "停用",
"workspace.reload.disabled.after": "启用后将在余额较低时自动充值。",
"workspace.reload.enabled.before": "自动充值已",
"workspace.reload.enabled.state": "已启用",
"workspace.reload.enabled.middle": "我们将重新加载",
"workspace.reload.processingFee": "加工费",
"workspace.reload.enabled.middle": "我们将自动充值",
"workspace.reload.processingFee": "手续费",
"workspace.reload.enabled.after": "当余额达到",
"workspace.reload.edit": "编辑",
"workspace.reload.enable": "使能够",
"workspace.reload.enableAutoReload": "启用自动重新加载",
"workspace.reload.reloadAmount": "重新加载 $",
"workspace.reload.enable": "启用",
"workspace.reload.enableAutoReload": "启用自动充值",
"workspace.reload.reloadAmount": "充值 $",
"workspace.reload.whenBalanceReaches": "当余额达到 $",
"workspace.reload.saving": "保存...",
"workspace.reload.save": "节省",
"workspace.reload.failedAt": "重新加载失败于",
"workspace.reload.save": "保存",
"workspace.reload.failedAt": "充值失败于",
"workspace.reload.reason": "原因:",
"workspace.reload.updatePaymentMethod": "请更新您的付款方式并重试。",
"workspace.reload.retrying": "正在重试...",
@@ -441,11 +441,11 @@ export const dict = {
"workspace.payments.subtitle": "最近的付款交易。",
"workspace.payments.table.date": "日期",
"workspace.payments.table.paymentId": "付款ID",
"workspace.payments.table.amount": "数量",
"workspace.payments.table.amount": "金额",
"workspace.payments.table.receipt": "收据",
"workspace.payments.type.credit": "信用",
"workspace.payments.type.subscription": "订阅",
"workspace.payments.view": "看",
"workspace.payments.view": "看",
"workspace.black.loading": "加载中...",
"workspace.black.time.day": "天",
"workspace.black.time.days": "天",
@@ -455,20 +455,20 @@ export const dict = {
"workspace.black.time.minutes": "分钟",
"workspace.black.time.fewSeconds": "几秒钟",
"workspace.black.subscription.title": "订阅",
"workspace.black.subscription.message": "您已订阅 OpenCode Black每月费用为 {{plan}} 美元。",
"workspace.black.subscription.message": "您已订阅 OpenCode Black费用为每月 ${{plan}}。",
"workspace.black.subscription.manage": "管理订阅",
"workspace.black.subscription.rollingUsage": "5小时使用",
"workspace.black.subscription.weeklyUsage": "每周使用量",
"workspace.black.subscription.resetsIn": "重置于",
"workspace.black.subscription.useBalance": "达到使用限额后使用您的可用余额",
"workspace.black.waitlist.title": "候补名单",
"workspace.black.waitlist.joined": "您正在等待每月 ${{plan}} OpenCode 黑色计划。",
"workspace.black.waitlist.ready": "我们已准备好您加入每月 {{plan}} 美元的 OpenCode 黑色计划。",
"workspace.black.waitlist.joined": "您已加入每月 ${{plan}} OpenCode Black 方案候补名单。",
"workspace.black.waitlist.ready": "我们已准备好您加入每月 ${{plan}} 的 OpenCode Black 方案。",
"workspace.black.waitlist.leave": "离开候补名单",
"workspace.black.waitlist.leaving": "离开...",
"workspace.black.waitlist.left": "左边",
"workspace.black.waitlist.enroll": "注册",
"workspace.black.waitlist.enrolling": "正在报名...",
"workspace.black.waitlist.enrolled": "已注册",
"workspace.black.waitlist.left": "已退出",
"workspace.black.waitlist.enroll": "加入",
"workspace.black.waitlist.enrolling": "加入中...",
"workspace.black.waitlist.enrolled": "已加入",
"workspace.black.waitlist.enrollNote": "单击“注册”后,您的订阅将立即开始,并且将从您的卡中扣费。",
} satisfies Dict

View File

@@ -293,9 +293,9 @@ export const dict = {
"changelog.hero.subtitle": "OpenCode \u7684\u65b0\u66f4\u65b0\u8207\u6539\u5584",
"changelog.empty": "\u627e\u4e0d\u5230\u66f4\u65b0\u65e5\u8a8c\u9805\u76ee\u3002",
"changelog.viewJson": "\u6aa2\u8996 JSON",
"workspace.nav.zen": "",
"workspace.nav.zen": "Zen",
"workspace.nav.apiKeys": "API 鍵",
"workspace.nav.members": "員",
"workspace.nav.members": "員",
"workspace.nav.billing": "計費",
"workspace.nav.settings": "設定",
"workspace.home.banner.beforeLink": "編碼代理的可靠優化模型。",
@@ -310,26 +310,26 @@ export const dict = {
"workspace.newUser.feature.lockin.body":
"將 Zen 與任何編碼代理結合使用,並在需要時繼續將其他提供程序與 opencode 結合使用。",
"workspace.newUser.copyApiKey": "複製 API 密鑰",
"workspace.newUser.copyKey": "複製鑰",
"workspace.newUser.copied": "複製",
"workspace.newUser.copyKey": "複製鑰",
"workspace.newUser.copied": "複製!",
"workspace.newUser.step.enableBilling": "啟用計費",
"workspace.newUser.step.login.before": "跑步",
"workspace.newUser.step.login.before": "執行",
"workspace.newUser.step.login.after": "並選擇 opencode",
"workspace.newUser.step.pasteKey": "粘貼您的 API 密鑰",
"workspace.newUser.step.models.before": "啟動 opencode 並運行",
"workspace.newUser.step.models.after": "選擇型",
"workspace.models.title": "型",
"workspace.newUser.step.models.after": "選擇型",
"workspace.models.title": "型",
"workspace.models.subtitle.beforeLink": "管理工作區成員可以訪問哪些模型。",
"workspace.models.table.model": "模型",
"workspace.models.table.enabled": "啟用",
"workspace.providers.title": "帶上你自己的鑰匙",
"workspace.providers.title": "自帶密鑰",
"workspace.providers.subtitle": "從 AI 提供商處配置您自己的 API 密鑰。",
"workspace.providers.placeholder": "輸入 {{provider}} API 密鑰({{prefix}}...",
"workspace.providers.configure": "配置",
"workspace.providers.edit": "編輯",
"workspace.providers.delete": "刪除",
"workspace.providers.saving": "保存...",
"workspace.providers.save": "節省",
"workspace.providers.save": "儲存",
"workspace.providers.table.provider": "提供者",
"workspace.providers.table.apiKey": "API 密鑰",
"workspace.usage.title": "使用歷史",
@@ -348,25 +348,25 @@ export const dict = {
"workspace.usage.subscription": "訂閱 (${{amount}})",
"workspace.cost.title": "成本",
"workspace.cost.subtitle": "按型號細分的使用成本。",
"workspace.cost.allModels": "所有型",
"workspace.cost.allKeys": "所有按鍵",
"workspace.cost.allModels": "所有型",
"workspace.cost.allKeys": "所有密鑰",
"workspace.cost.deletedSuffix": "(已刪除)",
"workspace.cost.empty": "所選期間沒有可用的使用數據。",
"workspace.cost.subscriptionShort": "",
"workspace.cost.subscriptionShort": "",
"workspace.keys.title": "API 鍵",
"workspace.keys.subtitle": "管理您的 API 密鑰以訪問 opencode 服務。",
"workspace.keys.create": "創建 API 密鑰",
"workspace.keys.placeholder": "輸入按鍵名稱",
"workspace.keys.placeholder": "輸入密鑰名稱",
"workspace.keys.empty": "創建 opencode 網關 API 密鑰",
"workspace.keys.table.name": "名",
"workspace.keys.table.key": "鑰",
"workspace.keys.table.name": "名",
"workspace.keys.table.key": "鑰",
"workspace.keys.table.createdBy": "創建者",
"workspace.keys.table.lastUsed": "最後使用",
"workspace.keys.copyApiKey": "複製 API 密鑰",
"workspace.keys.delete": "刪除",
"workspace.members.title": "員",
"workspace.members.title": "員",
"workspace.members.subtitle": "管理工作區成員及其權限。",
"workspace.members.invite": "邀請員",
"workspace.members.invite": "邀請員",
"workspace.members.inviting": "邀請...",
"workspace.members.beta.beforeLink": "測試期間,工作空間對團隊免費。",
"workspace.members.form.invitee": "受邀者",
@@ -379,11 +379,11 @@ export const dict = {
"workspace.members.edit": "編輯",
"workspace.members.delete": "刪除",
"workspace.members.saving": "保存...",
"workspace.members.save": "節省",
"workspace.members.save": "儲存",
"workspace.members.table.email": "電子郵件",
"workspace.members.table.role": "角色",
"workspace.members.table.monthLimit": "月份限制",
"workspace.members.role.admin": "行政",
"workspace.members.table.monthLimit": "月限額",
"workspace.members.role.admin": "管理員",
"workspace.members.role.adminDescription": "可以管理模型、成員和計費",
"workspace.members.role.member": "成員",
"workspace.members.role.memberDescription": "只能為自己生成 API 密鑰",
@@ -392,7 +392,7 @@ export const dict = {
"workspace.settings.workspaceName": "工作區名稱",
"workspace.settings.defaultName": "預設",
"workspace.settings.updating": "更新中...",
"workspace.settings.save": "節省",
"workspace.settings.save": "儲存",
"workspace.settings.edit": "編輯",
"workspace.billing.title": "計費",
"workspace.billing.subtitle.beforeLink": "管理付款方式。",
@@ -404,35 +404,35 @@ export const dict = {
"workspace.billing.loading": "載入中...",
"workspace.billing.addAction": "添加",
"workspace.billing.addBalance": "添加餘額",
"workspace.billing.linkedToStripe": "鏈接到條紋",
"workspace.billing.linkedToStripe": "已連結 Stripe",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "啟用計費",
"workspace.monthlyLimit.title": "每月限額",
"workspace.monthlyLimit.subtitle": "為您的帳戶設置每月使用限額。",
"workspace.monthlyLimit.placeholder": "50",
"workspace.monthlyLimit.setting": "環境...",
"workspace.monthlyLimit.set": "",
"workspace.monthlyLimit.setting": "設定中...",
"workspace.monthlyLimit.set": "設定",
"workspace.monthlyLimit.edit": "編輯限制",
"workspace.monthlyLimit.noLimit": "沒有設置使用限制。",
"workspace.monthlyLimit.currentUsage.beforeMonth": "當前使用情況為",
"workspace.monthlyLimit.currentUsage.beforeAmount": " $",
"workspace.reload.title": "自動重新加載",
"workspace.reload.disabled.before": "自動重新加載是",
"workspace.reload.disabled.state": "殘疾人",
"workspace.reload.disabled.after": "啟用餘額不足時自動值。",
"workspace.reload.enabled.before": "自動重新加載是",
"workspace.monthlyLimit.currentUsage.beforeMonth": "當前",
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $",
"workspace.reload.title": "自動儲值",
"workspace.reload.disabled.before": "自動儲值已",
"workspace.reload.disabled.state": "停用",
"workspace.reload.disabled.after": "啟用後會在餘額偏低時自動值。",
"workspace.reload.enabled.before": "自動儲值已",
"workspace.reload.enabled.state": "已啟用",
"workspace.reload.enabled.middle": "我們將重新加載",
"workspace.reload.processingFee": "加工費",
"workspace.reload.enabled.middle": "我們將自動儲值",
"workspace.reload.processingFee": "手續費",
"workspace.reload.enabled.after": "當餘額達到",
"workspace.reload.edit": "編輯",
"workspace.reload.enable": "使能夠",
"workspace.reload.enableAutoReload": "啟用自動重新加載",
"workspace.reload.reloadAmount": "重新加載 $",
"workspace.reload.enable": "啟用",
"workspace.reload.enableAutoReload": "啟用自動儲值",
"workspace.reload.reloadAmount": "儲值 $",
"workspace.reload.whenBalanceReaches": "當餘額達到 $",
"workspace.reload.saving": "保存...",
"workspace.reload.save": "節省",
"workspace.reload.failedAt": "重新加載失敗於",
"workspace.reload.save": "儲存",
"workspace.reload.failedAt": "儲值失敗於",
"workspace.reload.reason": "原因:",
"workspace.reload.updatePaymentMethod": "請更新您的付款方式並重試。",
"workspace.reload.retrying": "正在重試...",
@@ -441,11 +441,11 @@ export const dict = {
"workspace.payments.subtitle": "最近的付款交易。",
"workspace.payments.table.date": "日期",
"workspace.payments.table.paymentId": "付款ID",
"workspace.payments.table.amount": "數量",
"workspace.payments.table.amount": "金額",
"workspace.payments.table.receipt": "收據",
"workspace.payments.type.credit": "信用",
"workspace.payments.type.subscription": "訂閱",
"workspace.payments.view": "看",
"workspace.payments.view": "看",
"workspace.black.loading": "載入中...",
"workspace.black.time.day": "天",
"workspace.black.time.days": "天",
@@ -455,20 +455,20 @@ export const dict = {
"workspace.black.time.minutes": "分鐘",
"workspace.black.time.fewSeconds": "幾秒鐘",
"workspace.black.subscription.title": "訂閱",
"workspace.black.subscription.message": "您已訂閱 OpenCode Black每月費用為 {{plan}} 美元。",
"workspace.black.subscription.message": "您已訂閱 OpenCode Black費用為每月 ${{plan}}。",
"workspace.black.subscription.manage": "管理訂閱",
"workspace.black.subscription.rollingUsage": "5小時使用",
"workspace.black.subscription.weeklyUsage": "每週使用量",
"workspace.black.subscription.resetsIn": "重置於",
"workspace.black.subscription.useBalance": "達到使用限額後使用您的可用餘額",
"workspace.black.waitlist.title": "候補名單",
"workspace.black.waitlist.joined": "您正在等待每月 ${{plan}} OpenCode 黑色計劃。",
"workspace.black.waitlist.ready": "我們已準備好您加入每月 {{plan}} 美元的 OpenCode 黑色計劃。",
"workspace.black.waitlist.joined": "您已加入每月 ${{plan}} OpenCode Black 方案候補名單。",
"workspace.black.waitlist.ready": "我們已準備好您加入每月 ${{plan}} 的 OpenCode Black 方案。",
"workspace.black.waitlist.leave": "離開候補名單",
"workspace.black.waitlist.leaving": "離開...",
"workspace.black.waitlist.left": "左邊",
"workspace.black.waitlist.enroll": "註冊",
"workspace.black.waitlist.enrolling": "正在報名...",
"workspace.black.waitlist.enrolled": "已註冊",
"workspace.black.waitlist.left": "已退出",
"workspace.black.waitlist.enroll": "加入",
"workspace.black.waitlist.enrolling": "加入中...",
"workspace.black.waitlist.enrolled": "已加入",
"workspace.black.waitlist.enrollNote": "單擊“註冊”後,您的訂閱將立即開始,並且將從您的卡中扣費。",
} satisfies Dict

View File

@@ -68,6 +68,82 @@ const TAG = {
tr: "tr",
} satisfies Record<Locale, string>
const DOCS = {
en: "root",
zh: "zh-cn",
zht: "zh-tw",
ko: "ko",
de: "de",
es: "es",
fr: "fr",
it: "it",
da: "da",
ja: "ja",
pl: "pl",
ru: "ru",
ar: "ar",
no: "nb",
br: "pt-br",
th: "th",
tr: "tr",
} satisfies Record<Locale, string>
const DOCS_SEGMENT = new Set([
"ar",
"bs",
"da",
"de",
"es",
"fr",
"it",
"ja",
"ko",
"nb",
"pl",
"pt-br",
"ru",
"th",
"tr",
"zh-cn",
"zh-tw",
])
function suffix(pathname: string) {
const index = pathname.search(/[?#]/)
if (index === -1) {
return {
path: fix(pathname),
suffix: "",
}
}
return {
path: fix(pathname.slice(0, index)),
suffix: pathname.slice(index),
}
}
export function docs(locale: Locale, pathname: string) {
const value = DOCS[locale]
const next = suffix(pathname)
if (next.path !== "/docs" && next.path !== "/docs/" && !next.path.startsWith("/docs/")) {
return `${next.path}${next.suffix}`
}
if (value === "root") return `${next.path}${next.suffix}`
if (next.path === "/docs") return `/docs/${value}${next.suffix}`
if (next.path === "/docs/") return `/docs/${value}/${next.suffix}`
const head = next.path.slice("/docs/".length).split("/")[0] ?? ""
if (!head) return `/docs/${value}/${next.suffix}`
if (DOCS_SEGMENT.has(head)) return `${next.path}${next.suffix}`
if (head.startsWith("_")) return `${next.path}${next.suffix}`
if (head.includes(".")) return `${next.path}${next.suffix}`
return `/docs/${value}${next.path.slice("/docs".length)}${next.suffix}`
}
export function parseLocale(value: unknown): Locale | null {
if (typeof value !== "string") return null
if ((LOCALES as readonly string[]).includes(value)) return value as Locale
@@ -90,7 +166,7 @@ export function strip(pathname: string) {
export function route(locale: Locale, pathname: string) {
const next = strip(pathname)
if (next.startsWith("/docs")) return next
if (next.startsWith("/docs")) return docs(locale, next)
if (next.startsWith("/auth")) return next
if (next.startsWith("/workspace")) return next
if (locale === "en") return next

View File

@@ -1,14 +1,16 @@
import type { APIEvent } from "@solidjs/start/server"
import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
import { Resource } from "@opencode-ai/console-resource"
import { docs, localeFromRequest, tag } from "~/lib/language"
async function handler(evt: APIEvent) {
const req = evt.request.clone()
const url = new URL(req.url)
const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
const locale = localeFromRequest(req)
const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai"
const targetUrl = `https://${host}${docs(locale, url.pathname)}${url.search}`
const headers = new Headers(req.headers)
const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
if (locale) headers.set("accept-language", tag(locale))
headers.set("accept-language", tag(locale))
const response = await fetch(targetUrl, {
method: req.method,

View File

@@ -1,14 +1,16 @@
import type { APIEvent } from "@solidjs/start/server"
import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
import { Resource } from "@opencode-ai/console-resource"
import { docs, localeFromRequest, tag } from "~/lib/language"
async function handler(evt: APIEvent) {
const req = evt.request.clone()
const url = new URL(req.url)
const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
const locale = localeFromRequest(req)
const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai"
const targetUrl = `https://${host}${docs(locale, url.pathname)}${url.search}`
const headers = new Headers(req.headers)
const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
if (locale) headers.set("accept-language", tag(locale))
headers.set("accept-language", tag(locale))
const response = await fetch(targetUrl, {
method: req.method,

View File

@@ -294,7 +294,7 @@ export default function Download() {
</span>
<span>VS Code</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
<a href={language.route("/docs/ide/")} data-component="action-button">
{i18n.t("download.action.install")}
</a>
</div>
@@ -318,7 +318,7 @@ export default function Download() {
</span>
<span>Cursor</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
<a href={language.route("/docs/ide/")} data-component="action-button">
{i18n.t("download.action.install")}
</a>
</div>
@@ -335,7 +335,7 @@ export default function Download() {
</span>
<span>Zed</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
<a href={language.route("/docs/ide/")} data-component="action-button">
{i18n.t("download.action.install")}
</a>
</div>
@@ -352,7 +352,7 @@ export default function Download() {
</span>
<span>Windsurf</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
<a href={language.route("/docs/ide/")} data-component="action-button">
{i18n.t("download.action.install")}
</a>
</div>
@@ -369,7 +369,7 @@ export default function Download() {
</span>
<span>VSCodium</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
<a href={language.route("/docs/ide/")} data-component="action-button">
{i18n.t("download.action.install")}
</a>
</div>
@@ -393,7 +393,7 @@ export default function Download() {
</span>
<span>GitHub</span>
</div>
<a href="https://opencode.ai/docs/github/" data-component="action-button">
<a href={language.route("/docs/github/")} data-component="action-button">
{i18n.t("download.action.install")}
</a>
</div>
@@ -410,7 +410,7 @@ export default function Download() {
</span>
<span>GitLab</span>
</div>
<a href="https://opencode.ai/docs/gitlab/" data-component="action-button">
<a href={language.route("/docs/gitlab/")} data-component="action-button">
{i18n.t("download.action.install")}
</a>
</div>

View File

@@ -1,14 +1,16 @@
import type { APIEvent } from "@solidjs/start/server"
import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
import { Resource } from "@opencode-ai/console-resource"
import { docs, localeFromRequest, tag } from "~/lib/language"
async function handler(evt: APIEvent) {
const req = evt.request.clone()
const url = new URL(req.url)
const targetUrl = `https://docs.opencode.ai/docs${url.pathname}${url.search}`
const locale = localeFromRequest(req)
const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai"
const targetUrl = `https://${host}${docs(locale, `/docs${url.pathname}`)}${url.search}`
const headers = new Headers(req.headers)
const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
if (locale) headers.set("accept-language", tag(locale))
headers.set("accept-language", tag(locale))
const response = await fetch(targetUrl, {
method: req.method,

View File

@@ -9,10 +9,12 @@ import { GraphSection } from "./graph-section"
import { IconLogo } from "~/component/icon"
import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
export default function () {
const params = useParams()
const i18n = useI18n()
const language = useLanguage()
const userInfo = createAsync(() => querySessionInfo(params.id!))
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
const checkoutAction = useAction(createCheckoutUrl)
@@ -38,7 +40,7 @@ export default function () {
<p>
<span>
{i18n.t("workspace.home.banner.beforeLink")}{" "}
<a target="_blank" href="/docs/zen">
<a target="_blank" href={language.route("/docs/zen")}>
{i18n.t("common.learnMore")}
</a>
.

View File

@@ -12,7 +12,7 @@
"@opencode-ai/console-resource": "workspace:*",
"@planetscale/database": "1.19.0",
"aws4fetch": "1.0.20",
"drizzle-orm": "0.41.0",
"drizzle-orm": "catalog:",
"postgres": "3.4.7",
"stripe": "18.0.0",
"ulid": "catalog:",
@@ -43,7 +43,7 @@
"@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.0",
"@types/node": "catalog:",
"drizzle-kit": "0.30.5",
"drizzle-kit": "catalog:",
"mysql2": "3.14.4",
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"

View File

@@ -9,7 +9,7 @@ const stage = process.argv[2]
if (!stage) throw new Error("Stage is required")
const root = path.resolve(process.cwd(), "..", "..", "..")
const PARTS = 10
const PARTS = 20
// read the secret
const ret = await $`bun sst secret list`.cwd(root).text()

View File

@@ -9,7 +9,7 @@ const stage = process.argv[2]
if (!stage) throw new Error("Stage is required")
const root = path.resolve(process.cwd(), "..", "..", "..")
const PARTS = 10
const PARTS = 20
// read the secret
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()

View File

@@ -7,7 +7,7 @@ import { ZenData } from "../src/model"
const root = path.resolve(process.cwd(), "..", "..", "..")
const models = await $`bun sst secret list`.cwd(root).text()
const PARTS = 10
const PARTS = 20
// read the line starting with "ZEN_MODELS"
const lines = models.split("\n")

View File

@@ -4,7 +4,6 @@ export * from "drizzle-orm"
import { Client } from "@planetscale/database"
import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core"
import type { ExtractTablesWithRelations } from "drizzle-orm"
import type { PlanetScalePreparedQueryHKT, PlanetscaleQueryResultHKT } from "drizzle-orm/planetscale-serverless"
import { Context } from "../context"
import { memo } from "../util/memo"
@@ -14,7 +13,7 @@ export namespace Database {
PlanetscaleQueryResultHKT,
PlanetScalePreparedQueryHKT,
Record<string, never>,
ExtractTablesWithRelations<Record<string, never>>
any
>
const client = memo(() => {
@@ -23,7 +22,7 @@ export namespace Database {
username: Resource.Database.username,
password: Resource.Database.password,
})
const db = drizzle(result, {})
const db = drizzle({ client: result })
return db
})

View File

@@ -86,7 +86,17 @@ export namespace ZenData {
Resource.ZEN_MODELS7.value +
Resource.ZEN_MODELS8.value +
Resource.ZEN_MODELS9.value +
Resource.ZEN_MODELS10.value,
Resource.ZEN_MODELS10.value +
Resource.ZEN_MODELS11.value +
Resource.ZEN_MODELS12.value +
Resource.ZEN_MODELS13.value +
Resource.ZEN_MODELS14.value +
Resource.ZEN_MODELS15.value +
Resource.ZEN_MODELS16.value +
Resource.ZEN_MODELS17.value +
Resource.ZEN_MODELS18.value +
Resource.ZEN_MODELS19.value +
Resource.ZEN_MODELS20.value,
)
return ModelsSchema.parse(json)
})

View File

@@ -137,10 +137,50 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS11": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS12": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS13": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS14": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS15": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS16": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS17": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS18": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS19": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS20": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -137,10 +137,50 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS11": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS12": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS13": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS14": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS15": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS16": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS17": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS18": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS19": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS20": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -137,10 +137,50 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS11": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS12": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS13": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS14": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS15": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS16": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS17": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS18": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS19": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS20": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2.9.5", features = ["macos-private-api", "devtools"] }
tauri = { version = "2.9.5", features = ["macos-private-api"] }
tauri-plugin-opener = "2"
tauri-plugin-deep-link = "2.4.6"
tauri-plugin-shell = "2"
@@ -56,6 +56,7 @@ webkit2gtk = "=2.0.2"
objc2 = "0.6"
objc2-web-kit = "0.3"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61", features = [
"Win32_Foundation",

View File

@@ -2,6 +2,8 @@ mod cli;
mod constants;
#[cfg(windows)]
mod job_object;
#[cfg(target_os = "linux")]
pub mod linux_display;
mod markdown;
mod server;
mod window_customizer;
@@ -194,6 +196,43 @@ fn check_macos_app(app_name: &str) -> bool {
.unwrap_or(false)
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub enum LinuxDisplayBackend {
Wayland,
Auto,
}
#[tauri::command]
#[specta::specta]
fn get_display_backend() -> Option<LinuxDisplayBackend> {
#[cfg(target_os = "linux")]
{
let prefer = linux_display::read_wayland().unwrap_or(false);
return Some(if prefer {
LinuxDisplayBackend::Wayland
} else {
LinuxDisplayBackend::Auto
});
}
#[cfg(not(target_os = "linux"))]
None
}
#[tauri::command]
#[specta::specta]
fn set_display_backend(_app: AppHandle, _backend: LinuxDisplayBackend) -> Result<(), String> {
#[cfg(target_os = "linux")]
{
let prefer = matches!(_backend, LinuxDisplayBackend::Wayland);
return linux_display::write_wayland(&_app, prefer);
}
#[cfg(not(target_os = "linux"))]
Ok(())
}
#[cfg(target_os = "linux")]
fn check_linux_app(app_name: &str) -> bool {
return true;
@@ -209,6 +248,8 @@ pub fn run() {
await_initialization,
server::get_default_server_url,
server::set_default_server_url,
get_display_backend,
set_display_backend,
markdown::parse_markdown_command,
check_app_exists
])

View File

@@ -0,0 +1,47 @@
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::path::PathBuf;
use tauri::AppHandle;
use tauri_plugin_store::StoreExt;
use crate::constants::SETTINGS_STORE;
pub const LINUX_DISPLAY_CONFIG_KEY: &str = "linuxDisplayConfig";
#[derive(Default, Serialize, Deserialize)]
struct DisplayConfig {
wayland: Option<bool>,
}
fn dir() -> Option<PathBuf> {
Some(dirs::data_dir()?.join("ai.opencode.desktop"))
}
fn path() -> Option<PathBuf> {
dir().map(|dir| dir.join(SETTINGS_STORE))
}
pub fn read_wayland() -> Option<bool> {
let path = path()?;
let raw = std::fs::read_to_string(path).ok()?;
let config = serde_json::from_str::<DisplayConfig>(&raw).ok()?;
config.wayland
}
pub fn write_wayland(app: &AppHandle, value: bool) -> Result<(), String> {
let store = app
.store(SETTINGS_STORE)
.map_err(|e| format!("Failed to open settings store: {}", e))?;
store.set(
LINUX_DISPLAY_CONFIG_KEY,
json!(DisplayConfig {
wayland: Some(value),
}),
);
store
.save()
.map_err(|e| format!("Failed to save settings store: {}", e))?;
Ok(())
}

View File

@@ -23,12 +23,16 @@ fn configure_display_backend() -> Option<String> {
return None;
}
// Allow users to explicitly keep Wayland if they know their setup is stable.
let allow_wayland = matches!(
env::var("OC_ALLOW_WAYLAND"),
Ok(v) if matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes")
);
let prefer_wayland = opencode_lib::linux_display::read_wayland().unwrap_or(false);
let allow_wayland = prefer_wayland
|| matches!(
env::var("OC_ALLOW_WAYLAND"),
Ok(v) if matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes")
);
if allow_wayland {
if prefer_wayland {
return Some("Wayland session detected; using native Wayland from settings".into());
}
return Some("Wayland session detected; respecting OC_ALLOW_WAYLAND=1".into());
}

View File

@@ -10,6 +10,8 @@ export const commands = {
awaitInitialization: (events: Channel) => __TAURI_INVOKE<ServerReadyData>("await_initialization", { events }),
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
getDisplayBackend: () => __TAURI_INVOKE<"wayland" | "auto" | null>("get_display_backend"),
setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE<null>("set_display_backend", { backend }),
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
};
@@ -22,6 +24,8 @@ export const events = {
/* Types */
export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" };
export type LinuxDisplayBackend = "wayland" | "auto";
export type LoadingWindowComplete = null;
export type ServerReadyData = {

View File

@@ -1,7 +1,14 @@
// @refresh reload
import { webviewZoom } from "./webview-zoom"
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface, PlatformProvider, Platform, useCommand } from "@opencode-ai/app"
import {
AppBaseProviders,
AppInterface,
PlatformProvider,
Platform,
DisplayBackend,
useCommand,
} from "@opencode-ai/app"
import { open, save } from "@tauri-apps/plugin-dialog"
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener"
@@ -9,6 +16,7 @@ import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { type as ostype } from "@tauri-apps/plugin-os"
import { check, Update } from "@tauri-apps/plugin-updater"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { invoke } from "@tauri-apps/api/core"
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
import { relaunch } from "@tauri-apps/plugin-process"
import { AsyncStorage } from "@solid-primitives/storage"
@@ -338,6 +346,15 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
await commands.setDefaultServerUrl(url)
},
getDisplayBackend: async () => {
const result = await invoke<DisplayBackend | null>("get_display_backend").catch(() => null)
return result
},
setDisplayBackend: async (backend) => {
await invoke("set_display_backend", { backend }).catch(() => undefined)
},
parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown),
webviewZoom,
@@ -413,7 +430,7 @@ render(() => {
}
return (
<AppInterface defaultUrl={data().url}>
<AppInterface defaultUrl={data().url} isSidecar>
<Inner />
</AppInterface>
)

View File

@@ -137,10 +137,50 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS11": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS12": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS13": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS14": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS15": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS16": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS17": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS18": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS19": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS20": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -137,10 +137,50 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS11": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS12": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS13": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS14": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS15": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS16": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS17": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS18": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS19": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS20": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -1,27 +1,10 @@
# opencode agent guidelines
# opencode database guide
## Build/Test Commands
## Database
- **Install**: `bun install`
- **Run**: `bun run --conditions=browser ./src/index.ts`
- **Typecheck**: `bun run typecheck` (npm run typecheck)
- **Test**: `bun test` (runs all tests)
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
## Code Style
- **Runtime**: Bun with TypeScript ESM modules
- **Imports**: Use relative imports for local modules, named imports preferred
- **Types**: Zod schemas for validation, TypeScript interfaces for structure
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
## Architecture
- **Tools**: Implement `Tool.Info` interface with `execute()` method
- **Context**: Pass `sessionID` in tool context, use `App.provide()` for DI
- **Validation**: All inputs validated with Zod schemas
- **Logging**: Use `Log.create({ service: "name" })` pattern
- **Storage**: Use `Storage` namespace for persistence
- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files.
- **Schema**: Drizzle schema lives in `src/**/*.sql.ts`.
- **Naming**: tables and columns use snake*case; join columns are `<entity>_id`; indexes are `<table>*<column>\_idx`.
- **Migrations**: generated by Drizzle Kit using `drizzle.config.ts` (schema: `./src/**/*.sql.ts`, output: `./migration`).
- **Command**: `bun run db generate --name <slug>`.
- **Output**: creates `migration/<timestamp>_<slug>/migration.sql` and `snapshot.json`.
- **Tests**: migration tests should read the per-folder layout (no `_journal.json`).

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit"
export default defineConfig({
dialect: "sqlite",
schema: "./src/**/*.sql.ts",
out: "./migration",
dbCredentials: {
url: "/home/thdxr/.local/share/opencode/opencode.db",
},
})

View File

@@ -0,0 +1,90 @@
CREATE TABLE `project` (
`id` text PRIMARY KEY,
`worktree` text NOT NULL,
`vcs` text,
`name` text,
`icon_url` text,
`icon_color` text,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`time_initialized` integer,
`sandboxes` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `message` (
`id` text PRIMARY KEY,
`session_id` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `part` (
`id` text PRIMARY KEY,
`message_id` text NOT NULL,
`session_id` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `permission` (
`project_id` text PRIMARY KEY,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `session` (
`id` text PRIMARY KEY,
`project_id` text NOT NULL,
`parent_id` text,
`slug` text NOT NULL,
`directory` text NOT NULL,
`title` text NOT NULL,
`version` text NOT NULL,
`share_url` text,
`summary_additions` integer,
`summary_deletions` integer,
`summary_files` integer,
`summary_diffs` text,
`revert` text,
`permission` text,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`time_compacting` integer,
`time_archived` integer,
CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `todo` (
`session_id` text NOT NULL,
`content` text NOT NULL,
`status` text NOT NULL,
`priority` text NOT NULL,
`position` integer NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `position`),
CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `session_share` (
`session_id` text PRIMARY KEY,
`id` text NOT NULL,
`secret` text NOT NULL,
`url` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint
CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint
CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint
CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint
CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint
CREATE INDEX `todo_session_idx` ON `todo` (`session_id`);

View File

@@ -0,0 +1,796 @@
{
"version": "7",
"dialect": "sqlite",
"id": "068758ed-a97a-46f6-8a59-6c639ae7c20c",
"prevIds": ["00000000-0000-0000-0000-000000000000"],
"ddl": [
{
"name": "project",
"entityType": "tables"
},
{
"name": "message",
"entityType": "tables"
},
{
"name": "part",
"entityType": "tables"
},
{
"name": "permission",
"entityType": "tables"
},
{
"name": "session",
"entityType": "tables"
},
{
"name": "todo",
"entityType": "tables"
},
{
"name": "session_share",
"entityType": "tables"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "worktree",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "vcs",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_url",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_color",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_initialized",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "sandboxes",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "message_id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "parent_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "slug",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "title",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "version",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "share_url",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_additions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_deletions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_files",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_diffs",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "revert",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "permission",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_compacting",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_archived",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "content",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "status",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "priority",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "position",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "secret",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session_share"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_message_session_id_session_id_fk",
"entityType": "fks",
"table": "message"
},
{
"columns": ["message_id"],
"tableTo": "message",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_part_message_id_message_id_fk",
"entityType": "fks",
"table": "part"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_permission_project_id_project_id_fk",
"entityType": "fks",
"table": "permission"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_project_id_project_id_fk",
"entityType": "fks",
"table": "session"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_todo_session_id_session_id_fk",
"entityType": "fks",
"table": "todo"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_share_session_id_session_id_fk",
"entityType": "fks",
"table": "session_share"
},
{
"columns": ["session_id", "position"],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": ["project_id"],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": ["session_id"],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
"entityType": "pks"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "message_session_idx",
"entityType": "indexes",
"table": "message"
},
{
"columns": [
{
"value": "message_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_message_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_session_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "project_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_project_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "parent_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_parent_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "todo_session_idx",
"entityType": "indexes",
"table": "todo"
}
],
"renames": []
}

View File

@@ -15,7 +15,8 @@
"lint": "echo 'Running lint checks...' && bun test --coverage",
"format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
"docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'"
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'",
"db": "bun drizzle-kit"
},
"bin": {
"opencode": "./bin/opencode"
@@ -42,6 +43,8 @@
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"@typescript/native-preview": "catalog:",
"drizzle-kit": "1.0.0-beta.12-a5629fb",
"drizzle-orm": "1.0.0-beta.12-a5629fb",
"typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"why-is-node-running": "3.2.2",
@@ -100,6 +103,7 @@
"clipboardy": "4.0.0",
"decimal.js": "10.5.0",
"diff": "catalog:",
"drizzle-orm": "1.0.0-beta.12-a5629fb",
"fuzzysort": "3.1.0",
"gray-matter": "4.0.3",
"hono": "catalog:",
@@ -122,5 +126,8 @@
"yargs": "18.0.0",
"zod": "catalog:",
"zod-to-json-schema": "3.24.5"
},
"overrides": {
"drizzle-orm": "1.0.0-beta.12-a5629fb"
}
}

View File

@@ -25,6 +25,32 @@ await Bun.write(
)
console.log("Generated models-snapshot.ts")
// Load migrations from migration directories
const migrationDirs = (await fs.promises.readdir(path.join(dir, "migration"), { withFileTypes: true }))
.filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
.map((entry) => entry.name)
.sort()
const migrations = await Promise.all(
migrationDirs.map(async (name) => {
const file = path.join(dir, "migration", name, "migration.sql")
const sql = await Bun.file(file).text()
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
const timestamp = match
? Date.UTC(
Number(match[1]),
Number(match[2]) - 1,
Number(match[3]),
Number(match[4]),
Number(match[5]),
Number(match[6]),
)
: 0
return { sql, timestamp }
}),
)
console.log(`Loaded ${migrations.length} migrations`)
const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
@@ -156,6 +182,7 @@ for (const item of targets) {
entrypoints: ["./src/index.ts", parserWorker, workerPath],
define: {
OPENCODE_VERSION: `'${Script.version}'`,
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
OPENCODE_WORKER_PATH: workerPath,
OPENCODE_CHANNEL: `'${Script.channel}'`,

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bun
import { $ } from "bun"
// drizzle-kit check compares schema to migrations, exits non-zero if drift
const result = await $`bun drizzle-kit check`.quiet().nothrow()
if (result.exitCode !== 0) {
console.error("Schema has changes not captured in migrations!")
console.error("Run: bun drizzle-kit generate")
console.error("")
console.error(result.stderr.toString())
process.exit(1)
}
console.log("Migrations are up to date")

View File

@@ -228,8 +228,8 @@ export namespace ACP {
const metadata = permission.metadata || {}
const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
const content = await Bun.file(filepath).text()
const file = Bun.file(filepath)
const content = (await file.exists()) ? await file.text() : ""
const newContent = getNewContent(content, diff)
if (newContent) {
@@ -435,46 +435,68 @@ export namespace ACP {
return
}
}
return
}
if (part.type === "text") {
const delta = props.delta
if (delta && part.ignored !== true) {
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: delta,
},
case "message.part.delta": {
const props = event.properties
const session = this.sessionManager.tryGet(props.sessionID)
if (!session) return
const sessionId = session.id
const message = await this.sdk.session
.message(
{
sessionID: props.sessionID,
messageID: props.messageID,
directory: session.cwd,
},
{ throwOnError: true },
)
.then((x) => x.data)
.catch((error) => {
log.error("unexpected error when fetching message", { error })
return undefined
})
if (!message || message.info.role !== "assistant") return
const part = message.parts.find((p) => p.id === props.partID)
if (!part) return
if (part.type === "text" && props.field === "text" && part.ignored !== true) {
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: props.delta,
},
})
.catch((error) => {
log.error("failed to send text to ACP", { error })
})
}
},
})
.catch((error) => {
log.error("failed to send text delta to ACP", { error })
})
return
}
if (part.type === "reasoning") {
const delta = props.delta
if (delta) {
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_thought_chunk",
content: {
type: "text",
text: delta,
},
if (part.type === "reasoning" && props.field === "text") {
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_thought_chunk",
content: {
type: "text",
text: props.delta,
},
})
.catch((error) => {
log.error("failed to send reasoning to ACP", { error })
})
}
},
})
.catch((error) => {
log.error("failed to send reasoning delta to ACP", { error })
})
}
return
}

View File

@@ -3,7 +3,8 @@ import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
import { Session } from "../../session"
import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap"
import { Storage } from "../../storage/storage"
import { Database } from "../../storage/db"
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
import { Instance } from "../../project/instance"
import { ShareNext } from "../../share/share-next"
import { EOL } from "os"
@@ -130,13 +131,35 @@ export const ImportCommand = cmd({
return
}
await Storage.write(["session", Instance.project.id, exportData.info.id], exportData.info)
Database.use((db) => db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run())
for (const msg of exportData.messages) {
await Storage.write(["message", exportData.info.id, msg.info.id], msg.info)
Database.use((db) =>
db
.insert(MessageTable)
.values({
id: msg.info.id,
session_id: exportData.info.id,
time_created: msg.info.time?.created ?? Date.now(),
data: msg.info,
})
.onConflictDoNothing()
.run(),
)
for (const part of msg.parts) {
await Storage.write(["part", msg.info.id, part.id], part)
Database.use((db) =>
db
.insert(PartTable)
.values({
id: part.id,
message_id: msg.info.id,
session_id: exportData.info.id,
data: part,
})
.onConflictDoNothing()
.run(),
)
}
}

View File

@@ -2,7 +2,8 @@ import type { Argv } from "yargs"
import { cmd } from "./cmd"
import { Session } from "../../session"
import { bootstrap } from "../bootstrap"
import { Storage } from "../../storage/storage"
import { Database } from "../../storage/db"
import { SessionTable } from "../../session/session.sql"
import { Project } from "../../project/project"
import { Instance } from "../../project/instance"
@@ -87,25 +88,8 @@ async function getCurrentProject(): Promise<Project.Info> {
}
async function getAllSessions(): Promise<Session.Info[]> {
const sessions: Session.Info[] = []
const projectKeys = await Storage.list(["project"])
const projects = await Promise.all(projectKeys.map((key) => Storage.read<Project.Info>(key)))
for (const project of projects) {
if (!project) continue
const sessionKeys = await Storage.list(["session", project.id])
const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key)))
for (const session of projectSessions) {
if (session) {
sessions.push(session)
}
}
}
return sessions
const rows = Database.use((db) => db.select().from(SessionTable).all())
return rows.map((row) => Session.fromRow(row))
}
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {

View File

@@ -299,6 +299,24 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}
case "message.part.delta": {
const parts = store.part[event.properties.messageID]
if (!parts) break
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (!result.found) break
setStore(
"part",
event.properties.messageID,
produce((draft) => {
const part = draft[result.index]
const field = event.properties.field as keyof typeof part
const existing = part[field] as string | undefined
;(part[field] as string) = (existing ?? "") + event.properties.delta
}),
)
break
}
case "message.part.removed": {
const parts = store.part[event.properties.messageID]
const result = Binary.search(parts, event.properties.partID, (p) => p.id)

View File

@@ -1099,9 +1099,6 @@ export function Session() {
sessionID={route.sessionID}
/>
</box>
<Show when={!sidebarVisible() || !wide()}>
<Footer />
</Show>
</Show>
<Toast />
</box>
@@ -2029,8 +2026,8 @@ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
</For>
</Match>
<Match when={true}>
<InlineTool icon="%" pending="Preparing apply_patch..." complete={false} part={props.part}>
apply_patch
<InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}>
Patch
</InlineTool>
</Match>
</Switch>

View File

@@ -26,6 +26,10 @@ import { EOL } from "os"
import { WebCommand } from "./cli/cmd/web"
import { PrCommand } from "./cli/cmd/pr"
import { SessionCommand } from "./cli/cmd/session"
import path from "path"
import { Global } from "./global"
import { JsonMigration } from "./storage/json-migration"
import { Database } from "./storage/db"
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
@@ -74,6 +78,37 @@ const cli = yargs(hideBin(process.argv))
version: Installation.VERSION,
args: process.argv.slice(2),
})
const marker = path.join(Global.Path.data, "opencode.db")
if (!(await Bun.file(marker).exists())) {
console.log("Performing one time database migration, may take a few minutes...")
const tty = process.stdout.isTTY
const width = 36
const orange = "\x1b[38;5;214m"
const muted = "\x1b[0;2m"
const reset = "\x1b[0m"
let last = -1
if (tty) process.stdout.write("\x1b[?25l")
try {
await JsonMigration.run(Database.Client().$client, {
progress: (event) => {
if (!tty) return
const percent = Math.floor((event.current / event.total) * 100)
if (percent === last && event.current !== event.total) return
last = percent
const fill = Math.round((percent / 100) * width)
const bar = `${"■".repeat(fill)}${"・".repeat(width - fill)}`
process.stdout.write(
`\r${orange}${bar} ${percent.toString().padStart(3)}%${reset} ${muted}${event.label.padEnd(12)} ${event.current}/${event.total}${reset}`,
)
if (event.current === event.total) process.stdout.write("\n")
},
})
} finally {
if (tty) process.stdout.write("\x1b[?25h")
}
console.log("Database migration complete.")
}
})
.usage("\n" + UI.logo())
.completion("completion", "generate shell completion script")

View File

@@ -3,7 +3,8 @@ import { BusEvent } from "@/bus/bus-event"
import { Config } from "@/config/config"
import { Identifier } from "@/id/id"
import { Instance } from "@/project/instance"
import { Storage } from "@/storage/storage"
import { Database, eq } from "@/storage/db"
import { PermissionTable } from "@/session/session.sql"
import { fn } from "@/util/fn"
import { Log } from "@/util/log"
import { Wildcard } from "@/util/wildcard"
@@ -105,9 +106,12 @@ export namespace PermissionNext {
),
}
const state = Instance.state(async () => {
const state = Instance.state(() => {
const projectID = Instance.project.id
const stored = await Storage.read<Ruleset>(["permission", projectID]).catch(() => [] as Ruleset)
const row = Database.use((db) =>
db.select().from(PermissionTable).where(eq(PermissionTable.project_id, projectID)).get(),
)
const stored = row?.data ?? ([] as Ruleset)
const pending: Record<
string,
@@ -222,7 +226,8 @@ export namespace PermissionNext {
// TODO: we don't save the permission ruleset to disk yet until there's
// UI to manage it
// await Storage.write(["permission", Instance.project.id], s.approved)
// db().insert(PermissionTable).values({ projectID: Instance.project.id, data: s.approved })
// .onConflictDoUpdate({ target: PermissionTable.projectID, set: { data: s.approved } }).run()
return
}
},
@@ -275,6 +280,7 @@ export namespace PermissionNext {
}
export async function list() {
return state().then((x) => Object.values(x.pending).map((x) => x.info))
const s = await state()
return Object.values(s.pending).map((x) => x.info)
}
}

View File

@@ -1,5 +1,4 @@
import { Plugin } from "../plugin"
import { Share } from "../share/share"
import { Format } from "../format"
import { LSP } from "../lsp"
import { FileWatcher } from "../file/watcher"
@@ -17,7 +16,6 @@ import { Truncate } from "../tool/truncation"
export async function InstanceBootstrap() {
Log.Default.info("bootstrapping", { directory: Instance.directory })
await Plugin.init()
Share.init()
ShareNext.init()
Format.init()
await LSP.init()

View File

@@ -0,0 +1,14 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
import { Timestamps } from "@/storage/schema.sql"
export const ProjectTable = sqliteTable("project", {
id: text().primaryKey(),
worktree: text().notNull(),
vcs: text(),
name: text(),
icon_url: text(),
icon_color: text(),
...Timestamps,
time_initialized: integer(),
sandboxes: text({ mode: "json" }).notNull().$type<string[]>(),
})

View File

@@ -1,18 +1,17 @@
import z from "zod"
import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
import path from "path"
import { $ } from "bun"
import { Storage } from "../storage/storage"
import { Database, eq } from "../storage/db"
import { ProjectTable } from "./project.sql"
import { SessionTable } from "../session/session.sql"
import { Log } from "../util/log"
import { Flag } from "@/flag/flag"
import { Session } from "../session"
import { work } from "../util/queue"
import { fn } from "@opencode-ai/util/fn"
import { BusEvent } from "@/bus/bus-event"
import { iife } from "@/util/iife"
import { GlobalBus } from "@/bus/global"
import { existsSync } from "fs"
export namespace Project {
const log = Log.create({ service: "project" })
@@ -50,66 +49,85 @@ export namespace Project {
Updated: BusEvent.define("project.updated", Info),
}
type Row = typeof ProjectTable.$inferSelect
export function fromRow(row: Row): Info {
const icon =
row.icon_url || row.icon_color
? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
: undefined
return {
id: row.id,
worktree: row.worktree,
vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
name: row.name ?? undefined,
icon,
time: {
created: row.time_created,
updated: row.time_updated,
initialized: row.time_initialized ?? undefined,
},
sandboxes: row.sandboxes,
}
}
export async function fromDirectory(directory: string) {
log.info("fromDirectory", { directory })
const { id, sandbox, worktree, vcs } = await iife(async () => {
const data = await iife(async () => {
const matches = Filesystem.up({ targets: [".git"], start: directory })
const git = await matches.next().then((x) => x.value)
await matches.return()
if (git) {
let sandbox = path.dirname(git)
const sandbox = path.dirname(git)
const bin = Bun.which("git")
const gitBinary = Bun.which("git")
// cached id calculation
let id = await Bun.file(path.join(git, "opencode"))
const cached = await Bun.file(path.join(git, "opencode"))
.text()
.then((x) => x.trim())
.catch(() => undefined)
if (!gitBinary) {
if (!bin) {
return {
id: id ?? "global",
id: cached ?? "global",
worktree: sandbox,
sandbox: sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
// generate id from root commit
if (!id) {
const roots = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
.cwd(sandbox)
.text()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
.catch(() => undefined)
if (!roots) {
return {
id: "global",
worktree: sandbox,
sandbox: sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
id = roots[0]
if (id) {
void Bun.file(path.join(git, "opencode"))
.write(id)
const roots = cached
? undefined
: await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
.cwd(sandbox)
.text()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
.catch(() => undefined)
if (!cached && !roots) {
return {
id: "global",
worktree: sandbox,
sandbox: sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
const id = cached ?? roots?.[0]
if (!cached && id) {
void Bun.file(path.join(git, "opencode"))
.write(id)
.catch(() => undefined)
}
if (!id) {
return {
id: "global",
@@ -136,33 +154,31 @@ export namespace Project {
}
}
sandbox = top
const worktree = await $`git rev-parse --git-common-dir`
const tree = await $`git rev-parse --git-common-dir`
.quiet()
.nothrow()
.cwd(sandbox)
.cwd(top)
.text()
.then((x) => {
const dirname = path.dirname(x.trim())
if (dirname === ".") return sandbox
if (dirname === ".") return top
return dirname
})
.catch(() => undefined)
if (!worktree) {
if (!tree) {
return {
id,
sandbox,
worktree: sandbox,
sandbox: top,
worktree: top,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
return {
id,
sandbox,
worktree,
sandbox: top,
worktree: tree,
vcs: "git",
}
}
@@ -175,47 +191,78 @@ export namespace Project {
}
})
let existing = await Storage.read<Info>(["project", id]).catch(() => undefined)
if (!existing) {
existing = {
id,
worktree,
vcs: vcs as Info["vcs"],
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get())
const existing = await iife(async () => {
if (row) return fromRow(row)
const fresh: Info = {
id: data.id,
worktree: data.worktree,
vcs: data.vcs as Info["vcs"],
sandboxes: [],
time: {
created: Date.now(),
updated: Date.now(),
},
}
if (id !== "global") {
await migrateFromGlobal(id, worktree)
if (data.id !== "global") {
await migrateFromGlobal(data.id, data.worktree)
}
}
// migrate old projects before sandboxes
if (!existing.sandboxes) existing.sandboxes = []
return fresh
})
if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing)
const result: Info = {
...existing,
worktree,
vcs: vcs as Info["vcs"],
worktree: data.worktree,
vcs: data.vcs as Info["vcs"],
time: {
...existing.time,
updated: Date.now(),
},
}
if (sandbox !== result.worktree && !result.sandboxes.includes(sandbox)) result.sandboxes.push(sandbox)
result.sandboxes = result.sandboxes.filter((x) => existsSync(x))
await Storage.write<Info>(["project", id], result)
if (data.sandbox !== result.worktree && !result.sandboxes.includes(data.sandbox))
result.sandboxes.push(data.sandbox)
const sandboxes: string[] = []
for (const x of result.sandboxes) {
const stat = await Bun.file(x)
.stat()
.catch(() => undefined)
if (stat) sandboxes.push(x)
}
result.sandboxes = sandboxes
const insert = {
id: result.id,
worktree: result.worktree,
vcs: result.vcs ?? null,
name: result.name,
icon_url: result.icon?.url,
icon_color: result.icon?.color,
time_created: result.time.created,
time_updated: result.time.updated,
time_initialized: result.time.initialized,
sandboxes: result.sandboxes,
}
const updateSet = {
worktree: result.worktree,
vcs: result.vcs ?? null,
name: result.name,
icon_url: result.icon?.url,
icon_color: result.icon?.color,
time_updated: result.time.updated,
time_initialized: result.time.initialized,
sandboxes: result.sandboxes,
}
Database.use((db) =>
db.insert(ProjectTable).values(insert).onConflictDoUpdate({ target: ProjectTable.id, set: updateSet }).run(),
)
GlobalBus.emit("event", {
payload: {
type: Event.Updated.type,
properties: result,
},
})
return { project: result, sandbox }
return { project: result, sandbox: data.sandbox }
}
export async function discover(input: Info) {
@@ -248,43 +295,54 @@ export namespace Project {
return
}
async function migrateFromGlobal(newProjectID: string, worktree: string) {
const globalProject = await Storage.read<Info>(["project", "global"]).catch(() => undefined)
if (!globalProject) return
async function migrateFromGlobal(id: string, worktree: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get())
if (!row) return
const globalSessions = await Storage.list(["session", "global"]).catch(() => [])
if (globalSessions.length === 0) return
const sessions = Database.use((db) =>
db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(),
)
if (sessions.length === 0) return
log.info("migrating sessions from global", { newProjectID, worktree, count: globalSessions.length })
log.info("migrating sessions from global", { newProjectID: id, worktree, count: sessions.length })
await work(10, globalSessions, async (key) => {
const sessionID = key[key.length - 1]
const session = await Storage.read<Session.Info>(key).catch(() => undefined)
if (!session) return
if (session.directory && session.directory !== worktree) return
await work(10, sessions, async (row) => {
// Skip sessions that belong to a different directory
if (row.directory && row.directory !== worktree) return
session.projectID = newProjectID
log.info("migrating session", { sessionID, from: "global", to: newProjectID })
await Storage.write(["session", newProjectID, sessionID], session)
await Storage.remove(key)
log.info("migrating session", { sessionID: row.id, from: "global", to: id })
Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run())
}).catch((error) => {
log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID })
log.error("failed to migrate sessions from global to project", { error, projectId: id })
})
}
export async function setInitialized(projectID: string) {
await Storage.update<Info>(["project", projectID], (draft) => {
draft.time.initialized = Date.now()
})
export function setInitialized(id: string) {
Database.use((db) =>
db
.update(ProjectTable)
.set({
time_initialized: Date.now(),
})
.where(eq(ProjectTable.id, id))
.run(),
)
}
export async function list() {
const keys = await Storage.list(["project"])
const projects = await Promise.all(keys.map((x) => Storage.read<Info>(x)))
return projects.map((project) => ({
...project,
sandboxes: project.sandboxes?.filter((x) => existsSync(x)),
}))
export function list() {
return Database.use((db) =>
db
.select()
.from(ProjectTable)
.all()
.map((row) => fromRow(row)),
)
}
export function get(id: string): Info | undefined {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) return undefined
return fromRow(row)
}
export const update = fn(
@@ -295,77 +353,89 @@ export namespace Project {
commands: Info.shape.commands.optional(),
}),
async (input) => {
const result = await Storage.update<Info>(["project", input.projectID], (draft) => {
if (input.name !== undefined) draft.name = input.name
if (input.icon !== undefined) {
draft.icon = {
...draft.icon,
}
if (input.icon.url !== undefined) draft.icon.url = input.icon.url
if (input.icon.override !== undefined) draft.icon.override = input.icon.override || undefined
if (input.icon.color !== undefined) draft.icon.color = input.icon.color
}
if (input.commands?.start !== undefined) {
const start = input.commands.start || undefined
draft.commands = {
...(draft.commands ?? {}),
}
draft.commands.start = start
if (!draft.commands.start) draft.commands = undefined
}
draft.time.updated = Date.now()
})
const result = Database.use((db) =>
db
.update(ProjectTable)
.set({
name: input.name,
icon_url: input.icon?.url,
icon_color: input.icon?.color,
time_updated: Date.now(),
})
.where(eq(ProjectTable.id, input.projectID))
.returning()
.get(),
)
if (!result) throw new Error(`Project not found: ${input.projectID}`)
const data = fromRow(result)
GlobalBus.emit("event", {
payload: {
type: Event.Updated.type,
properties: result,
properties: data,
},
})
return result
return data
},
)
export async function sandboxes(projectID: string) {
const project = await Storage.read<Info>(["project", projectID]).catch(() => undefined)
if (!project?.sandboxes) return []
export async function sandboxes(id: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) return []
const data = fromRow(row)
const valid: string[] = []
for (const dir of project.sandboxes) {
const stat = await fs.stat(dir).catch(() => undefined)
for (const dir of data.sandboxes) {
const stat = await Bun.file(dir)
.stat()
.catch(() => undefined)
if (stat?.isDirectory()) valid.push(dir)
}
return valid
}
export async function addSandbox(projectID: string, directory: string) {
const result = await Storage.update<Info>(["project", projectID], (draft) => {
const sandboxes = draft.sandboxes ?? []
if (!sandboxes.includes(directory)) sandboxes.push(directory)
draft.sandboxes = sandboxes
draft.time.updated = Date.now()
})
export async function addSandbox(id: string, directory: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) throw new Error(`Project not found: ${id}`)
const sandboxes = [...row.sandboxes]
if (!sandboxes.includes(directory)) sandboxes.push(directory)
const result = Database.use((db) =>
db
.update(ProjectTable)
.set({ sandboxes, time_updated: Date.now() })
.where(eq(ProjectTable.id, id))
.returning()
.get(),
)
if (!result) throw new Error(`Project not found: ${id}`)
const data = fromRow(result)
GlobalBus.emit("event", {
payload: {
type: Event.Updated.type,
properties: result,
properties: data,
},
})
return result
return data
}
export async function removeSandbox(projectID: string, directory: string) {
const result = await Storage.update<Info>(["project", projectID], (draft) => {
const sandboxes = draft.sandboxes ?? []
draft.sandboxes = sandboxes.filter((sandbox) => sandbox !== directory)
draft.time.updated = Date.now()
})
export async function removeSandbox(id: string, directory: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) throw new Error(`Project not found: ${id}`)
const sandboxes = row.sandboxes.filter((s) => s !== directory)
const result = Database.use((db) =>
db
.update(ProjectTable)
.set({ sandboxes, time_updated: Date.now() })
.where(eq(ProjectTable.id, id))
.returning()
.get(),
)
if (!result) throw new Error(`Project not found: ${id}`)
const data = fromRow(result)
GlobalBus.emit("event", {
payload: {
type: Event.Updated.type,
properties: result,
properties: data,
},
})
return result
return data
}
}

View File

@@ -0,0 +1,191 @@
import { APICallError } from "ai"
import { STATUS_CODES } from "http"
import { iife } from "@/util/iife"
export namespace ProviderError {
// Adapted from overflow detection patterns in:
// https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts
const OVERFLOW_PATTERNS = [
/prompt is too long/i, // Anthropic
/input is too long for requested model/i, // Amazon Bedrock
/exceeds the context window/i, // OpenAI (Completions + Responses API message text)
/input token count.*exceeds the maximum/i, // Google (Gemini)
/maximum prompt length is \d+/i, // xAI (Grok)
/reduce the length of the messages/i, // Groq
/maximum context length is \d+ tokens/i, // OpenRouter
/exceeds the limit of \d+/i, // GitHub Copilot
/exceeds the available context size/i, // llama.cpp server
/greater than the context length/i, // LM Studio
/context window exceeds limit/i, // MiniMax
/exceeded model token limit/i, // Kimi For Coding
/context[_ ]length[_ ]exceeded/i, // Generic fallback
/too many tokens/i, // Generic fallback
/token limit exceeded/i, // Generic fallback
]
function isOpenAiErrorRetryable(e: APICallError) {
const status = e.statusCode
if (!status) return e.isRetryable
// openai sometimes returns 404 for models that are actually available
return status === 404 || e.isRetryable
}
// Providers not reliably handled in this function:
// - z.ai: can accept overflow silently (needs token-count/context-window checks)
function isOverflow(message: string) {
if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true
// Providers/status patterns handled outside of regex list:
// - Cerebras: often returns "400 (no body)" / "413 (no body)"
// - Mistral: often returns "400 (no body)" / "413 (no body)"
return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
}
function error(providerID: string, error: APICallError) {
if (providerID.includes("github-copilot") && error.statusCode === 403) {
return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
}
return error.message
}
function message(providerID: string, e: APICallError) {
return iife(() => {
const msg = e.message
if (msg === "") {
if (e.responseBody) return e.responseBody
if (e.statusCode) {
const err = STATUS_CODES[e.statusCode]
if (err) return err
}
return "Unknown error"
}
const transformed = error(providerID, e)
if (transformed !== msg) {
return transformed
}
if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
return msg
}
try {
const body = JSON.parse(e.responseBody)
// try to extract common error message fields
const errMsg = body.message || body.error || body.error?.message
if (errMsg && typeof errMsg === "string") {
return `${msg}: ${errMsg}`
}
} catch {}
return `${msg}: ${e.responseBody}`
}).trim()
}
function json(input: unknown) {
if (typeof input === "string") {
try {
const result = JSON.parse(input)
if (result && typeof result === "object") return result
return undefined
} catch {
return undefined
}
}
if (typeof input === "object" && input !== null) {
return input
}
return undefined
}
export type ParsedStreamError =
| {
type: "context_overflow"
message: string
responseBody: string
}
| {
type: "api_error"
message: string
isRetryable: false
responseBody: string
}
export function parseStreamError(input: unknown): ParsedStreamError | undefined {
const body = json(input)
if (!body) return
const responseBody = JSON.stringify(body)
if (body.type !== "error") return
switch (body?.error?.code) {
case "context_length_exceeded":
return {
type: "context_overflow",
message: "Input exceeds context window of this model",
responseBody,
}
case "insufficient_quota":
return {
type: "api_error",
message: "Quota exceeded. Check your plan and billing details.",
isRetryable: false,
responseBody,
}
case "usage_not_included":
return {
type: "api_error",
message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
isRetryable: false,
responseBody,
}
case "invalid_prompt":
return {
type: "api_error",
message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
isRetryable: false,
responseBody,
}
}
}
export type ParsedAPICallError =
| {
type: "context_overflow"
message: string
responseBody?: string
}
| {
type: "api_error"
message: string
statusCode?: number
isRetryable: boolean
responseHeaders?: Record<string, string>
responseBody?: string
metadata?: Record<string, string>
}
export function parseAPICallError(input: { providerID: string; error: APICallError }): ParsedAPICallError {
const m = message(input.providerID, input.error)
if (isOverflow(m)) {
return {
type: "context_overflow",
message: m,
responseBody: input.error.responseBody,
}
}
const metadata = input.error.url ? { url: input.error.url } : undefined
return {
type: "api_error",
message: m,
statusCode: input.error.statusCode,
isRetryable: input.providerID.startsWith("openai")
? isOpenAiErrorRetryable(input.error)
: input.error.isRetryable,
responseHeaders: input.error.responseHeaders,
responseBody: input.error.responseBody,
metadata,
}
}
}

View File

@@ -1,4 +1,4 @@
import type { APICallError, ModelMessage } from "ai"
import type { ModelMessage } from "ai"
import { mergeDeep, unique } from "remeda"
import type { JSONSchema7 } from "@ai-sdk/provider"
import type { JSONSchema } from "zod/v4/core"
@@ -643,6 +643,20 @@ export namespace ProviderTransform {
}
}
// Enable thinking for reasoning models on alibaba-cn (DashScope).
// DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
// to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
// deepseek-r1, etc. never output thinking/reasoning tokens.
// Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
if (
input.model.providerID === "alibaba-cn" &&
input.model.capabilities.reasoning &&
input.model.api.npm === "@ai-sdk/openai-compatible" &&
!modelId.includes("kimi-k2-thinking")
) {
result["enable_thinking"] = true
}
if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
if (!input.model.api.id.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
@@ -810,19 +824,4 @@ export namespace ProviderTransform {
return schema as JSONSchema7
}
export function error(providerID: string, error: APICallError) {
let message = error.message
if (providerID.includes("github-copilot") && error.statusCode === 403) {
return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
}
if (providerID.includes("github-copilot") && message.includes("The requested model is not supported")) {
return (
message +
"\n\nMake sure the model is enabled in your copilot settings: https://github.com/settings/copilot/features"
)
}
return message
}
}

View File

@@ -1,6 +1,6 @@
import { resolver } from "hono-openapi"
import z from "zod"
import { Storage } from "../storage/storage"
import { NotFoundError } from "../storage/db"
export const ERRORS = {
400: {
@@ -25,7 +25,7 @@ export const ERRORS = {
description: "Not found",
content: {
"application/json": {
schema: resolver(Storage.NotFoundError.Schema),
schema: resolver(NotFoundError.Schema),
},
},
},

View File

@@ -3,7 +3,7 @@ import { describeRoute, validator, resolver } from "hono-openapi"
import { upgradeWebSocket } from "hono/bun"
import z from "zod"
import { Pty } from "@/pty"
import { Storage } from "../../storage/storage"
import { NotFoundError } from "../../storage/db"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
@@ -76,7 +76,7 @@ export const PtyRoutes = lazy(() =>
async (c) => {
const info = Pty.get(c.req.valid("param").ptyID)
if (!info) {
throw new Storage.NotFoundError({ message: "Session not found" })
throw new NotFoundError({ message: "Session not found" })
}
return c.json(info)
},

View File

@@ -276,18 +276,15 @@ export const SessionRoutes = lazy(() =>
const sessionID = c.req.valid("param").sessionID
const updates = c.req.valid("json")
const updatedSession = await Session.update(
sessionID,
(session) => {
if (updates.title !== undefined) {
session.title = updates.title
}
if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived
},
{ touch: false },
)
let session = await Session.get(sessionID)
if (updates.title !== undefined) {
session = await Session.setTitle({ sessionID, title: updates.title })
}
if (updates.time?.archived !== undefined) {
session = await Session.setArchived({ sessionID, time: updates.time.archived })
}
return c.json(updatedSession)
return c.json(session)
},
)
.post(

View File

@@ -31,7 +31,7 @@ import { ExperimentalRoutes } from "./routes/experimental"
import { ProviderRoutes } from "./routes/provider"
import { lazy } from "../util/lazy"
import { InstanceBootstrap } from "../project/bootstrap"
import { Storage } from "../storage/storage"
import { NotFoundError } from "../storage/db"
import type { ContentfulStatusCode } from "hono/utils/http-status"
import { websocket } from "hono/bun"
import { HTTPException } from "hono/http-exception"
@@ -65,7 +65,7 @@ export namespace Server {
})
if (err instanceof NamedError) {
let status: ContentfulStatusCode
if (err instanceof Storage.NotFoundError) status = 404
if (err instanceof NotFoundError) status = 404
else if (err instanceof Provider.ModelNotFoundError) status = 400
else if (err.name.startsWith("Worktree")) status = 400
else status = 500

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