summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorFrank <[email protected]>2026-03-04 11:13:14 -0500
committerFrank <[email protected]>2026-03-04 11:13:14 -0500
commite9de2505f65b65a581a90dbd3b29f21dac6568de (patch)
treeb4f982f67f5e1fd78debf192ad237806db2cd62d /packages
parent22fcde926fd8a071a213a54d624584372e73822d (diff)
parent715b844c2a88810b6178d7a2467c7d36ea8fb764 (diff)
downloadopencode-e9de2505f65b65a581a90dbd3b29f21dac6568de.tar.gz
opencode-e9de2505f65b65a581a90dbd3b29f21dac6568de.zip
Merge branch 'dev' into go-page
Diffstat (limited to 'packages')
-rw-r--r--packages/app/package.json2
-rw-r--r--packages/app/src/app.tsx11
-rw-r--r--packages/app/src/components/terminal.tsx45
-rw-r--r--packages/app/src/components/titlebar.tsx2
-rw-r--r--packages/app/src/pages/layout.tsx2
-rw-r--r--packages/app/src/pages/layout/sidebar-items.tsx8
-rw-r--r--packages/app/src/pages/session.tsx1
-rw-r--r--packages/app/src/pages/session/composer/session-composer-region.tsx43
-rw-r--r--packages/app/src/pages/session/composer/session-todo-dock.tsx7
-rw-r--r--packages/app/src/pages/session/message-timeline.tsx419
-rw-r--r--packages/app/src/pages/session/terminal-panel.tsx10
-rw-r--r--packages/app/src/utils/notification-click.test.ts37
-rw-r--r--packages/app/src/utils/notification-click.ts16
-rw-r--r--packages/console/app/package.json2
-rw-r--r--packages/console/app/src/routes/zen/util/provider/anthropic.ts2
-rw-r--r--packages/console/core/package.json2
-rw-r--r--packages/console/function/package.json2
-rw-r--r--packages/console/mail/package.json2
-rw-r--r--packages/desktop-electron/.gitignore28
-rw-r--r--packages/desktop-electron/AGENTS.md4
-rw-r--r--packages/desktop-electron/README.md32
-rw-r--r--packages/desktop-electron/electron-builder.config.ts97
-rw-r--r--packages/desktop-electron/electron.vite.config.ts41
-rw-r--r--packages/desktop-electron/icons/README.md11
-rw-r--r--packages/desktop-electron/icons/beta/128x128.pngbin0 -> 10186 bytes
-rw-r--r--packages/desktop-electron/icons/beta/[email protected]bin0 -> 36252 bytes
-rw-r--r--packages/desktop-electron/icons/beta/32x32.pngbin0 -> 1309 bytes
-rw-r--r--packages/desktop-electron/icons/beta/64x64.pngbin0 -> 3587 bytes
-rw-r--r--packages/desktop-electron/icons/beta/Square107x107Logo.pngbin0 -> 7562 bytes
-rw-r--r--packages/desktop-electron/icons/beta/Square142x142Logo.pngbin0 -> 12279 bytes
-rw-r--r--packages/desktop-electron/icons/beta/Square150x150Logo.pngbin0 -> 13445 bytes
-rw-r--r--packages/desktop-electron/icons/beta/Square284x284Logo.pngbin0 -> 45201 bytes
-rw-r--r--packages/desktop-electron/icons/beta/Square30x30Logo.pngbin0 -> 1281 bytes
-rw-r--r--packages/desktop-electron/icons/beta/Square310x310Logo.pngbin0 -> 54725 bytes
-rw-r--r--packages/desktop-electron/icons/beta/Square44x44Logo.pngbin0 -> 2167 bytes
-rw-r--r--packages/desktop-electron/icons/beta/Square71x71Logo.pngbin0 -> 4121 bytes
-rw-r--r--packages/desktop-electron/icons/beta/Square89x89Logo.pngbin0 -> 5782 bytes
-rw-r--r--packages/desktop-electron/icons/beta/StoreLogo.pngbin0 -> 2559 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher.pngbin0 -> 2077 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.pngbin0 -> 15269 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_round.pngbin0 -> 1887 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher.pngbin0 -> 2083 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.pngbin0 -> 7845 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_round.pngbin0 -> 1792 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher.pngbin0 -> 5778 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.pngbin0 -> 25523 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 5026 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher.pngbin0 -> 10758 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.pngbin0 -> 60763 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 9312 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 17122 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.pngbin0 -> 116520 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 14941 bytes
-rw-r--r--packages/desktop-electron/icons/beta/android/values/ic_launcher_background.xml4
-rw-r--r--packages/desktop-electron/icons/beta/icon.icnsbin0 -> 882048 bytes
-rw-r--r--packages/desktop-electron/icons/beta/icon.icobin0 -> 49612 bytes
-rw-r--r--packages/desktop-electron/icons/beta/icon.pngbin0 -> 172485 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 687 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 1660 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 1660 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 2950 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 1072 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 2834 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 2834 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 5048 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 1660 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 4396 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 4396 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 8452 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 596205 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 8452 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 16916 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 4193 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 12523 bytes
-rw-r--r--packages/desktop-electron/icons/beta/ios/[email protected]bin0 -> 14760 bytes
-rw-r--r--packages/desktop-electron/icons/dev/128x128.pngbin0 -> 16568 bytes
-rw-r--r--packages/desktop-electron/icons/dev/[email protected]bin0 -> 59884 bytes
-rw-r--r--packages/desktop-electron/icons/dev/32x32.pngbin0 -> 1973 bytes
-rw-r--r--packages/desktop-electron/icons/dev/64x64.pngbin0 -> 5469 bytes
-rw-r--r--packages/desktop-electron/icons/dev/Square107x107Logo.pngbin0 -> 12116 bytes
-rw-r--r--packages/desktop-electron/icons/dev/Square142x142Logo.pngbin0 -> 19936 bytes
-rw-r--r--packages/desktop-electron/icons/dev/Square150x150Logo.pngbin0 -> 21988 bytes
-rw-r--r--packages/desktop-electron/icons/dev/Square284x284Logo.pngbin0 -> 74022 bytes
-rw-r--r--packages/desktop-electron/icons/dev/Square30x30Logo.pngbin0 -> 1786 bytes
-rw-r--r--packages/desktop-electron/icons/dev/Square310x310Logo.pngbin0 -> 89075 bytes
-rw-r--r--packages/desktop-electron/icons/dev/Square44x44Logo.pngbin0 -> 3211 bytes
-rw-r--r--packages/desktop-electron/icons/dev/Square71x71Logo.pngbin0 -> 6370 bytes
-rw-r--r--packages/desktop-electron/icons/dev/Square89x89Logo.pngbin0 -> 9316 bytes
-rw-r--r--packages/desktop-electron/icons/dev/StoreLogo.pngbin0 -> 3862 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher.pngbin0 -> 3076 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.pngbin0 -> 24987 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_round.pngbin0 -> 2853 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher.pngbin0 -> 3016 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.pngbin0 -> 12682 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2702 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher.pngbin0 -> 8701 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.pngbin0 -> 42285 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 7640 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher.pngbin0 -> 16970 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.pngbin0 -> 97586 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 14939 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 27316 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.pngbin0 -> 180625 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 24066 bytes
-rw-r--r--packages/desktop-electron/icons/dev/android/values/ic_launcher_background.xml4
-rw-r--r--packages/desktop-electron/icons/dev/icon.icnsbin0 -> 1187792 bytes
-rw-r--r--packages/desktop-electron/icons/dev/icon.icobin0 -> 73182 bytes
-rw-r--r--packages/desktop-electron/icons/dev/icon.pngbin0 -> 264014 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 955 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 2695 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 2695 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 4932 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 1640 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 4684 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 4684 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 8781 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 2695 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 7529 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 7529 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 14557 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 980713 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 14557 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 29995 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 7093 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 22066 bytes
-rw-r--r--packages/desktop-electron/icons/dev/ios/[email protected]bin0 -> 25898 bytes
-rw-r--r--packages/desktop-electron/icons/prod/128x128.pngbin0 -> 9013 bytes
-rw-r--r--packages/desktop-electron/icons/prod/[email protected]bin0 -> 36840 bytes
-rw-r--r--packages/desktop-electron/icons/prod/32x32.pngbin0 -> 1255 bytes
-rw-r--r--packages/desktop-electron/icons/prod/64x64.pngbin0 -> 2971 bytes
-rw-r--r--packages/desktop-electron/icons/prod/Square107x107Logo.pngbin0 -> 6441 bytes
-rw-r--r--packages/desktop-electron/icons/prod/Square142x142Logo.pngbin0 -> 10850 bytes
-rw-r--r--packages/desktop-electron/icons/prod/Square150x150Logo.pngbin0 -> 12036 bytes
-rw-r--r--packages/desktop-electron/icons/prod/Square284x284Logo.pngbin0 -> 47137 bytes
-rw-r--r--packages/desktop-electron/icons/prod/Square30x30Logo.pngbin0 -> 1109 bytes
-rw-r--r--packages/desktop-electron/icons/prod/Square310x310Logo.pngbin0 -> 58165 bytes
-rw-r--r--packages/desktop-electron/icons/prod/Square44x44Logo.pngbin0 -> 1827 bytes
-rw-r--r--packages/desktop-electron/icons/prod/Square71x71Logo.pngbin0 -> 3405 bytes
-rw-r--r--packages/desktop-electron/icons/prod/Square89x89Logo.pngbin0 -> 4760 bytes
-rw-r--r--packages/desktop-electron/icons/prod/StoreLogo.pngbin0 -> 2186 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher.pngbin0 -> 1886 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.pngbin0 -> 13918 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_round.pngbin0 -> 1811 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher.pngbin0 -> 1873 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.pngbin0 -> 6540 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_round.pngbin0 -> 1751 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher.pngbin0 -> 4726 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.pngbin0 -> 25393 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 4101 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher.pngbin0 -> 9156 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.pngbin0 -> 64829 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 8270 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 15359 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.pngbin0 -> 127895 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 14064 bytes
-rw-r--r--packages/desktop-electron/icons/prod/android/values/ic_launcher_background.xml4
-rw-r--r--packages/desktop-electron/icons/prod/icon.icnsbin0 -> 1010901 bytes
-rw-r--r--packages/desktop-electron/icons/prod/icon.icobin0 -> 47600 bytes
-rw-r--r--packages/desktop-electron/icons/prod/icon.pngbin0 -> 190179 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 728 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 1607 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 1607 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 2648 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 1094 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 2542 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 2542 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 4709 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 1607 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 4058 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 4058 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 7828 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 681769 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 7828 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 17106 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 3730 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 12166 bytes
-rw-r--r--packages/desktop-electron/icons/prod/ios/[email protected]bin0 -> 14705 bytes
-rw-r--r--packages/desktop-electron/package.json52
-rw-r--r--packages/desktop-electron/resources/entitlements.plist30
-rw-r--r--packages/desktop-electron/scripts/copy-bundles.ts12
-rw-r--r--packages/desktop-electron/scripts/copy-icons.ts12
-rw-r--r--packages/desktop-electron/scripts/finalize-latest-yml.ts116
-rw-r--r--packages/desktop-electron/scripts/predev.ts17
-rwxr-xr-xpackages/desktop-electron/scripts/prepare.ts24
-rw-r--r--packages/desktop-electron/scripts/utils.ts69
-rw-r--r--packages/desktop-electron/src/main/apps.ts148
-rw-r--r--packages/desktop-electron/src/main/cli.ts279
-rw-r--r--packages/desktop-electron/src/main/constants.ts10
-rw-r--r--packages/desktop-electron/src/main/env.d.ts7
-rw-r--r--packages/desktop-electron/src/main/index.ts449
-rw-r--r--packages/desktop-electron/src/main/ipc.ts176
-rw-r--r--packages/desktop-electron/src/main/logging.ts40
-rw-r--r--packages/desktop-electron/src/main/markdown.ts16
-rw-r--r--packages/desktop-electron/src/main/menu.ts116
-rw-r--r--packages/desktop-electron/src/main/migrate.ts91
-rw-r--r--packages/desktop-electron/src/main/server.ts129
-rw-r--r--packages/desktop-electron/src/main/store.ts15
-rw-r--r--packages/desktop-electron/src/main/windows.ts135
-rw-r--r--packages/desktop-electron/src/preload/index.ts66
-rw-r--r--packages/desktop-electron/src/preload/types.ts64
-rw-r--r--packages/desktop-electron/src/renderer/cli.ts12
-rw-r--r--packages/desktop-electron/src/renderer/env.d.ts12
-rw-r--r--packages/desktop-electron/src/renderer/i18n/ar.ts26
-rw-r--r--packages/desktop-electron/src/renderer/i18n/br.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/bs.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/da.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/de.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/en.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/es.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/fr.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/index.ts187
-rw-r--r--packages/desktop-electron/src/renderer/i18n/ja.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/ko.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/no.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/pl.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/ru.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/zh.ts26
-rw-r--r--packages/desktop-electron/src/renderer/i18n/zht.ts26
-rw-r--r--packages/desktop-electron/src/renderer/index.html23
-rw-r--r--packages/desktop-electron/src/renderer/index.tsx312
-rw-r--r--packages/desktop-electron/src/renderer/loading.html23
-rw-r--r--packages/desktop-electron/src/renderer/loading.tsx80
-rw-r--r--packages/desktop-electron/src/renderer/styles.css0
-rw-r--r--packages/desktop-electron/src/renderer/updater.ts14
-rw-r--r--packages/desktop-electron/src/renderer/webview-zoom.ts38
-rw-r--r--packages/desktop-electron/sst-env.d.ts10
-rw-r--r--packages/desktop-electron/tsconfig.json22
-rw-r--r--packages/desktop/package.json2
-rw-r--r--packages/enterprise/package.json2
-rw-r--r--packages/extensions/zed/extension.toml12
-rw-r--r--packages/function/package.json2
-rw-r--r--packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql5
-rw-r--r--packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json1013
-rw-r--r--packages/opencode/package.json2
-rw-r--r--packages/opencode/src/auth/index.ts7
-rw-r--r--packages/opencode/src/cli/cmd/auth.ts7
-rw-r--r--packages/opencode/src/cli/cmd/run.ts39
-rw-r--r--packages/opencode/src/cli/cmd/serve.ts7
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx86
-rw-r--r--packages/opencode/src/cli/cmd/tui/thread.ts152
-rw-r--r--packages/opencode/src/cli/cmd/tui/worker.ts7
-rw-r--r--packages/opencode/src/config/config.ts13
-rw-r--r--packages/opencode/src/control-plane/adaptors/index.ts28
-rw-r--r--packages/opencode/src/control-plane/adaptors/types.ts7
-rw-r--r--packages/opencode/src/control-plane/adaptors/worktree.ts52
-rw-r--r--packages/opencode/src/control-plane/config.ts10
-rw-r--r--packages/opencode/src/control-plane/session-proxy-middleware.ts46
-rw-r--r--packages/opencode/src/control-plane/types.ts20
-rw-r--r--packages/opencode/src/control-plane/workspace-router-middleware.ts50
-rw-r--r--packages/opencode/src/control-plane/workspace-server/server.ts46
-rw-r--r--packages/opencode/src/control-plane/workspace.sql.ts6
-rw-r--r--packages/opencode/src/control-plane/workspace.ts108
-rw-r--r--packages/opencode/src/server/routes/experimental.ts2
-rw-r--r--packages/opencode/src/server/routes/session.ts2
-rw-r--r--packages/opencode/src/server/routes/workspace.ts18
-rw-r--r--packages/opencode/src/server/server.ts2
-rw-r--r--packages/opencode/src/tool/bash.txt2
-rw-r--r--packages/opencode/src/worktree/index.ts24
-rw-r--r--packages/opencode/test/auth/auth.test.ts58
-rw-r--r--packages/opencode/test/config/config.test.ts65
-rw-r--r--packages/opencode/test/control-plane/session-proxy-middleware.test.ts114
-rw-r--r--packages/opencode/test/control-plane/workspace-server-sse.test.ts7
-rw-r--r--packages/opencode/test/control-plane/workspace-sync.test.ts60
-rw-r--r--packages/plugin/package.json2
-rw-r--r--packages/sdk/js/package.json2
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts253
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts191
-rw-r--r--packages/sdk/openapi.json409
-rw-r--r--packages/slack/package.json2
-rw-r--r--packages/ui/package.json6
-rw-r--r--packages/ui/src/components/message-part.tsx23
-rw-r--r--packages/ui/src/styles/base.css11
-rw-r--r--packages/util/package.json2
-rw-r--r--packages/web/package.json2
-rw-r--r--packages/web/src/content/docs/ar/ecosystem.mdx103
-rw-r--r--packages/web/src/content/docs/bs/ecosystem.mdx75
-rw-r--r--packages/web/src/content/docs/da/ecosystem.mdx65
-rw-r--r--packages/web/src/content/docs/de/ecosystem.mdx65
-rw-r--r--packages/web/src/content/docs/ecosystem.mdx66
-rw-r--r--packages/web/src/content/docs/es/ecosystem.mdx65
-rw-r--r--packages/web/src/content/docs/fr/ecosystem.mdx103
-rw-r--r--packages/web/src/content/docs/go.mdx18
-rw-r--r--packages/web/src/content/docs/it/ecosystem.mdx69
-rw-r--r--packages/web/src/content/docs/ja/ecosystem.mdx66
-rw-r--r--packages/web/src/content/docs/ko/ecosystem.mdx65
-rw-r--r--packages/web/src/content/docs/nb/ecosystem.mdx65
-rw-r--r--packages/web/src/content/docs/pl/ecosystem.mdx113
-rw-r--r--packages/web/src/content/docs/pt-br/ecosystem.mdx91
-rw-r--r--packages/web/src/content/docs/ru/ecosystem.mdx104
-rw-r--r--packages/web/src/content/docs/th/ecosystem.mdx65
-rw-r--r--packages/web/src/content/docs/tr/ecosystem.mdx101
-rw-r--r--packages/web/src/content/docs/zen.mdx4
-rw-r--r--packages/web/src/content/docs/zh-cn/ecosystem.mdx65
-rw-r--r--packages/web/src/content/docs/zh-tw/ecosystem.mdx65
297 files changed, 6624 insertions, 1863 deletions
diff --git a/packages/app/package.json b/packages/app/package.json
index 446c14e96..ed497a761 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
- "version": "1.2.15",
+ "version": "1.2.17",
"description": "",
"type": "module",
"exports": {
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 4a25e8d94..52a1dac6a 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -7,8 +7,8 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { Font } from "@opencode-ai/ui/font"
import { ThemeProvider } from "@opencode-ai/ui/theme"
import { MetaProvider } from "@solidjs/meta"
-import { Navigate, Route, Router } from "@solidjs/router"
-import { ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
+import { BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
+import { Component, ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
import { CommandProvider } from "@/context/command"
import { CommentsProvider } from "@/context/comments"
import { FileProvider } from "@/context/file"
@@ -28,6 +28,7 @@ import { TerminalProvider } from "@/context/terminal"
import DirectoryLayout from "@/pages/directory-layout"
import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error"
+import { Dynamic } from "solid-js/web"
const Home = lazy(() => import("@/pages/home"))
const Session = lazy(() => import("@/pages/session"))
@@ -144,13 +145,15 @@ export function AppInterface(props: {
children?: JSX.Element
defaultServer: ServerConnection.Key
servers?: Array<ServerConnection.Any>
+ router?: Component<BaseRouterProps>
}) {
return (
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
<ServerKey>
<GlobalSDKProvider>
<GlobalSyncProvider>
- <Router
+ <Dynamic
+ component={props.router ?? Router}
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
>
<Route path="/" component={HomeRoute} />
@@ -158,7 +161,7 @@ export function AppInterface(props: {
<Route path="/" component={SessionIndexRoute} />
<Route path="/session/:id?" component={SessionRoute} />
</Route>
- </Router>
+ </Dynamic>
</GlobalSyncProvider>
</GlobalSDKProvider>
</ServerKey>
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
index 601ace28d..c27d6a977 100644
--- a/packages/app/src/components/terminal.tsx
+++ b/packages/app/src/components/terminal.tsx
@@ -18,7 +18,7 @@ const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
onSubmit?: () => void
- onCleanup?: (pty: LocalPTY) => void
+ onCleanup?: (pty: Partial<LocalPTY> & { id: string }) => void
onConnect?: () => void
onConnectError?: (error: unknown) => void
}
@@ -126,8 +126,8 @@ const persistTerminal = (input: {
term: Term | undefined
addon: SerializeAddon | undefined
cursor: number
- pty: LocalPTY
- onCleanup?: (pty: LocalPTY) => void
+ id: string
+ onCleanup?: (pty: Partial<LocalPTY> & { id: string }) => void
}) => {
if (!input.addon || !input.onCleanup || !input.term) return
const buffer = (() => {
@@ -140,7 +140,7 @@ const persistTerminal = (input: {
})()
input.onCleanup({
- ...input.pty,
+ id: input.id,
buffer,
cursor: input.cursor,
rows: input.term.rows,
@@ -158,6 +158,19 @@ export const Terminal = (props: TerminalProps) => {
const server = useServer()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
+ const id = local.pty.id
+ const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
+ const restoreSize =
+ restore &&
+ typeof local.pty.cols === "number" &&
+ Number.isSafeInteger(local.pty.cols) &&
+ local.pty.cols > 0 &&
+ typeof local.pty.rows === "number" &&
+ Number.isSafeInteger(local.pty.rows) &&
+ local.pty.rows > 0
+ ? { cols: local.pty.cols, rows: local.pty.rows }
+ : undefined
+ const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined
let ws: WebSocket | undefined
let term: Term | undefined
let ghostty: Ghostty
@@ -190,7 +203,7 @@ export const Terminal = (props: TerminalProps) => {
const pushSize = (cols: number, rows: number) => {
return sdk.client.pty
.update({
- ptyID: local.pty.id,
+ ptyID: id,
size: { cols, rows },
})
.catch((err) => {
@@ -319,18 +332,6 @@ export const Terminal = (props: TerminalProps) => {
const mod = loaded.mod
const g = loaded.ghostty
- const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
- const restoreSize =
- restore &&
- typeof local.pty.cols === "number" &&
- Number.isSafeInteger(local.pty.cols) &&
- local.pty.cols > 0 &&
- typeof local.pty.rows === "number" &&
- Number.isSafeInteger(local.pty.rows) &&
- local.pty.rows > 0
- ? { cols: local.pty.cols, rows: local.pty.rows }
- : undefined
-
const t = new mod.Terminal({
cursorBlink: true,
cursorStyle: "bar",
@@ -427,14 +428,14 @@ export const Terminal = (props: TerminalProps) => {
await write(restore)
fit.fit()
scheduleSize(t.cols, t.rows)
- if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
+ if (scrollY !== undefined) t.scrollToLine(scrollY)
startResize()
} else {
fit.fit()
scheduleSize(t.cols, t.rows)
if (restore) {
await write(restore)
- if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
+ if (scrollY !== undefined) t.scrollToLine(scrollY)
}
startResize()
}
@@ -446,9 +447,9 @@ export const Terminal = (props: TerminalProps) => {
const once = { value: false }
let closing = false
- const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
+ const url = new URL(sdk.url + `/pty/${id}/connect`)
url.searchParams.set("directory", sdk.directory)
- url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
+ url.searchParams.set("cursor", String(start !== undefined ? start : restore ? -1 : 0))
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
url.username = server.current?.http.username ?? ""
url.password = server.current?.http.password ?? ""
@@ -542,7 +543,7 @@ export const Terminal = (props: TerminalProps) => {
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000)
const finalize = () => {
- persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
+ persistTerminal({ term, addon: serializeAddon, cursor, id, onCleanup: props.onCleanup })
cleanup()
}
diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx
index 760f40fc0..c2b5a1ef4 100644
--- a/packages/app/src/components/titlebar.tsx
+++ b/packages/app/src/components/titlebar.tsx
@@ -157,6 +157,7 @@ export function Titlebar() {
<header
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
style={{ "min-height": minHeight() }}
+ data-tauri-drag-region
onMouseDown={drag}
onDblClick={maximize}
>
@@ -276,6 +277,7 @@ export function Titlebar() {
"flex items-center min-w-0 justify-end": true,
"pr-2": !windows(),
}}
+ data-tauri-drag-region
onMouseDown={drag}
>
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index daf2aaa5c..2fd2f2fe3 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -42,6 +42,7 @@ import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { playSound, soundSrc } from "@/utils/sound"
import { createAim } from "@/utils/aim"
+import { setNavigate } from "@/utils/notification-click"
import { Worktree as WorktreeState } from "@/utils/worktree"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -107,6 +108,7 @@ export default function Layout(props: ParentProps) {
const notification = useNotification()
const permission = usePermission()
const navigate = useNavigate()
+ setNavigate(navigate)
const providers = useProviders()
const dialog = useDialog()
const command = useCommand()
diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx
index eecfd17b5..0aaabc03b 100644
--- a/packages/app/src/pages/layout/sidebar-items.tsx
+++ b/packages/app/src/pages/layout/sidebar-items.tsx
@@ -6,7 +6,6 @@ import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { base64Encode } from "@opencode-ai/util/encode"
import { Avatar } from "@opencode-ai/ui/avatar"
-import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -137,13 +136,6 @@ const SessionRow = (props: {
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
{props.session.title}
</span>
- <Show when={props.session.summary}>
- {(summary) => (
- <div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
- <DiffChanges changes={summary()} />
- </div>
- )}
- </Show>
</div>
</A>
)
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 389c0baea..cc81ae7b6 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -1254,6 +1254,7 @@ export default function Page() {
<SessionComposerRegion
state={composer}
+ ready={!store.deferRender && messagesReady()}
centered={centered()}
inputRef={(el) => {
inputRef = el
diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx
index a882e25dd..93ea3d465 100644
--- a/packages/app/src/pages/session/composer/session-composer-region.tsx
+++ b/packages/app/src/pages/session/composer/session-composer-region.tsx
@@ -1,4 +1,5 @@
import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
+import { createStore } from "solid-js/store"
import { useParams } from "@solidjs/router"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { PromptInput } from "@/components/prompt-input"
@@ -12,6 +13,7 @@ import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
export function SessionComposerRegion(props: {
state: SessionComposerState
+ ready: boolean
centered: boolean
inputRef: (el: HTMLDivElement) => void
newSessionWorktree: string
@@ -61,7 +63,44 @@ export function SessionComposerRegion(props: {
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
})
- const open = createMemo(() => props.state.dock() && !props.state.closing())
+ const [gate, setGate] = createStore({
+ ready: false,
+ })
+ let timer: number | undefined
+ let frame: number | undefined
+
+ const clear = () => {
+ if (timer !== undefined) {
+ window.clearTimeout(timer)
+ timer = undefined
+ }
+ if (frame !== undefined) {
+ cancelAnimationFrame(frame)
+ frame = undefined
+ }
+ }
+
+ createEffect(() => {
+ sessionKey()
+ const ready = props.ready
+ const delay = 140
+
+ clear()
+ setGate("ready", false)
+ if (!ready) return
+
+ frame = requestAnimationFrame(() => {
+ frame = undefined
+ timer = window.setTimeout(() => {
+ setGate("ready", true)
+ timer = undefined
+ }, delay)
+ })
+ })
+
+ onCleanup(clear)
+
+ const open = createMemo(() => gate.ready && props.state.dock() && !props.state.closing())
const config = createMemo(() =>
open()
? {
@@ -76,7 +115,7 @@ export function SessionComposerRegion(props: {
const progress = useSpring(() => (open() ? 1 : 0), config)
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
const [height, setHeight] = createSignal(320)
- const dock = createMemo(() => props.state.dock() || value() > 0.001)
+ const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001)
const full = createMemo(() => Math.max(78, height()))
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
diff --git a/packages/app/src/pages/session/composer/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx
index ab8755aec..da2b8c8da 100644
--- a/packages/app/src/pages/session/composer/session-todo-dock.tsx
+++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx
@@ -281,10 +281,8 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
style={{
"--checkbox-align": "flex-start",
"--checkbox-offset": "1px",
- transition:
- "opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), filter 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
+ transition: "opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
opacity: todo().status === "pending" ? "0.94" : "1",
- filter: todo().status === "pending" ? "blur(0.3px)" : "blur(0px)",
}}
>
<TextStrikethrough
@@ -294,13 +292,12 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
style={{
"line-height": "var(--line-height-normal)",
transition:
- "color 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), filter 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
+ "color 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
color:
todo().status === "completed" || todo().status === "cancelled"
? "var(--text-weak)"
: "var(--text-strong)",
opacity: todo().status === "pending" ? "0.92" : "1",
- filter: todo().status === "pending" ? "blur(0.3px)" : "blur(0px)",
}}
/>
</Checkbox>
diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx
index a7f503d5a..433c36e2e 100644
--- a/packages/app/src/pages/session/message-timeline.tsx
+++ b/packages/app/src/pages/session/message-timeline.tsx
@@ -550,227 +550,228 @@ export function MessageTimeline(props: {
"--sticky-accordion-top": showHeader() ? "48px" : "0px",
}}
>
- <Show when={showHeader()}>
- <div
- data-session-title
- classList={{
- "sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
- "w-full": true,
- "pb-4": true,
- "pl-2 pr-3 md:pl-4 md:pr-3": true,
- "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
- }}
- >
- <div class="h-12 w-full flex items-center justify-between gap-2">
- <div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
- <Show when={parentID()}>
- <IconButton
- tabIndex={-1}
- icon="arrow-left"
- variant="ghost"
- onClick={navigateParent}
- aria-label={language.t("common.goBack")}
- />
- </Show>
- <Show when={titleValue() || title.editing}>
- <Show
- when={title.editing}
- fallback={
- <h1
- class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
- onDblClick={openTitleEditor}
- >
- {titleValue()}
- </h1>
- }
- >
- <InlineInput
- ref={(el) => {
- titleRef = el
- }}
- value={title.draft}
- disabled={title.saving}
- class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
- style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
- onInput={(event) => setTitle("draft", event.currentTarget.value)}
- onKeyDown={(event) => {
- event.stopPropagation()
- if (event.key === "Enter") {
- event.preventDefault()
- void saveTitleEditor()
- return
- }
- if (event.key === "Escape") {
- event.preventDefault()
- closeTitleEditor()
- }
- }}
- onBlur={closeTitleEditor}
+ <div ref={props.setContentRef} class="min-w-0 w-full">
+ <Show when={showHeader()}>
+ <div
+ data-session-title
+ classList={{
+ "sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
+ "w-full": true,
+ "pb-4": true,
+ "pl-2 pr-3 md:pl-4 md:pr-3": true,
+ "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
+ }}
+ >
+ <div class="h-12 w-full flex items-center justify-between gap-2">
+ <div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
+ <Show when={parentID()}>
+ <IconButton
+ tabIndex={-1}
+ icon="arrow-left"
+ variant="ghost"
+ onClick={navigateParent}
+ aria-label={language.t("common.goBack")}
/>
</Show>
- </Show>
- </div>
- <Show when={sessionID()}>
- {(id) => (
- <div class="shrink-0 flex items-center gap-3">
- <SessionContextUsage placement="bottom" />
- <DropdownMenu
- gutter={4}
- placement="bottom-end"
- open={title.menuOpen}
- onOpenChange={(open) => setTitle("menuOpen", open)}
+ <Show when={titleValue() || title.editing}>
+ <Show
+ when={title.editing}
+ fallback={
+ <h1
+ class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
+ onDblClick={openTitleEditor}
+ >
+ {titleValue()}
+ </h1>
+ }
>
- <DropdownMenu.Trigger
- as={IconButton}
- icon="dot-grid"
- variant="ghost"
- class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
- aria-label={language.t("common.moreOptions")}
- />
- <DropdownMenu.Portal>
- <DropdownMenu.Content
- style={{ "min-width": "104px" }}
- onCloseAutoFocus={(event) => {
- if (!title.pendingRename) return
+ <InlineInput
+ ref={(el) => {
+ titleRef = el
+ }}
+ value={title.draft}
+ disabled={title.saving}
+ class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
+ style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
+ onInput={(event) => setTitle("draft", event.currentTarget.value)}
+ onKeyDown={(event) => {
+ event.stopPropagation()
+ if (event.key === "Enter") {
event.preventDefault()
- setTitle("pendingRename", false)
- openTitleEditor()
- }}
- >
- <DropdownMenu.Item
- onSelect={() => {
- setTitle("pendingRename", true)
- setTitle("menuOpen", false)
+ void saveTitleEditor()
+ return
+ }
+ if (event.key === "Escape") {
+ event.preventDefault()
+ closeTitleEditor()
+ }
+ }}
+ onBlur={closeTitleEditor}
+ />
+ </Show>
+ </Show>
+ </div>
+ <Show when={sessionID()}>
+ {(id) => (
+ <div class="shrink-0 flex items-center gap-3">
+ <SessionContextUsage placement="bottom" />
+ <DropdownMenu
+ gutter={4}
+ placement="bottom-end"
+ open={title.menuOpen}
+ onOpenChange={(open) => setTitle("menuOpen", open)}
+ >
+ <DropdownMenu.Trigger
+ as={IconButton}
+ icon="dot-grid"
+ variant="ghost"
+ class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
+ aria-label={language.t("common.moreOptions")}
+ />
+ <DropdownMenu.Portal>
+ <DropdownMenu.Content
+ style={{ "min-width": "104px" }}
+ onCloseAutoFocus={(event) => {
+ if (!title.pendingRename) return
+ event.preventDefault()
+ setTitle("pendingRename", false)
+ openTitleEditor()
}}
>
- <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
- </DropdownMenu.Item>
- <DropdownMenu.Item onSelect={() => void archiveSession(id())}>
- <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
- </DropdownMenu.Item>
- <DropdownMenu.Separator />
- <DropdownMenu.Item
- onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
- >
- <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
- </DropdownMenu.Item>
- </DropdownMenu.Content>
- </DropdownMenu.Portal>
- </DropdownMenu>
- </div>
- )}
- </Show>
- </div>
- </div>
- </Show>
-
- <div
- ref={props.setContentRef}
- role="log"
- class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
- classList={{
- "w-full": true,
- "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
- "mt-0.5": props.centered,
- "mt-0": !props.centered,
- }}
- >
- <Show when={props.turnStart > 0 || props.historyMore}>
- <div class="w-full flex justify-center">
- <Button
- variant="ghost"
- size="large"
- class="text-12-medium opacity-50"
- disabled={props.historyLoading}
- onClick={props.onLoadEarlier}
- >
- {props.historyLoading
- ? language.t("session.messages.loadingEarlier")
- : language.t("session.messages.loadEarlier")}
- </Button>
+ <DropdownMenu.Item
+ onSelect={() => {
+ setTitle("pendingRename", true)
+ setTitle("menuOpen", false)
+ }}
+ >
+ <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
+ </DropdownMenu.Item>
+ <DropdownMenu.Item onSelect={() => void archiveSession(id())}>
+ <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
+ </DropdownMenu.Item>
+ <DropdownMenu.Separator />
+ <DropdownMenu.Item
+ onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
+ >
+ <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
+ </DropdownMenu.Item>
+ </DropdownMenu.Content>
+ </DropdownMenu.Portal>
+ </DropdownMenu>
+ </div>
+ )}
+ </Show>
+ </div>
</div>
</Show>
- <For each={rendered()}>
- {(messageID) => {
- const active = createMemo(() => activeMessageID() === messageID)
- const queued = createMemo(() => {
- if (active()) return false
- const activeID = activeMessageID()
- if (activeID) return messageID > activeID
- return false
- })
- const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], {
- equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
- })
- const commentCount = createMemo(() => comments().length)
- return (
- <div
- id={props.anchor(messageID)}
- data-message-id={messageID}
- ref={(el) => {
- props.onRegisterMessage(el, messageID)
- onCleanup(() => props.onUnregisterMessage(messageID))
- }}
- classList={{
- "min-w-0 w-full max-w-full": true,
- "md:max-w-200 2xl:max-w-[1000px]": props.centered,
- }}
+
+ <div
+ role="log"
+ class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
+ classList={{
+ "w-full": true,
+ "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
+ "mt-0.5": props.centered,
+ "mt-0": !props.centered,
+ }}
+ >
+ <Show when={props.turnStart > 0 || props.historyMore}>
+ <div class="w-full flex justify-center">
+ <Button
+ variant="ghost"
+ size="large"
+ class="text-12-medium opacity-50"
+ disabled={props.historyLoading}
+ onClick={props.onLoadEarlier}
>
- <Show when={commentCount() > 0}>
- <div class="w-full px-4 md:px-5 pb-2">
- <div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
- <div class="flex w-max min-w-full justify-end gap-2">
- <Index each={comments()}>
- {(commentAccessor: () => MessageComment) => {
- const comment = createMemo(() => commentAccessor())
- return (
- <div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
- <div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
- <FileIcon
- node={{ path: comment().path, type: "file" }}
- class="size-3.5 shrink-0"
- />
- <span class="truncate">{getFilename(comment().path)}</span>
- <Show when={comment().selection}>
- {(selection) => (
- <span class="shrink-0 text-text-weak">
- {selection().startLine === selection().endLine
- ? `:${selection().startLine}`
- : `:${selection().startLine}-${selection().endLine}`}
- </span>
- )}
- </Show>
- </div>
- <div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words">
- {comment().comment}
+ {props.historyLoading
+ ? language.t("session.messages.loadingEarlier")
+ : language.t("session.messages.loadEarlier")}
+ </Button>
+ </div>
+ </Show>
+ <For each={rendered()}>
+ {(messageID) => {
+ const active = createMemo(() => activeMessageID() === messageID)
+ const queued = createMemo(() => {
+ if (active()) return false
+ const activeID = activeMessageID()
+ if (activeID) return messageID > activeID
+ return false
+ })
+ const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], {
+ equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
+ })
+ const commentCount = createMemo(() => comments().length)
+ return (
+ <div
+ id={props.anchor(messageID)}
+ data-message-id={messageID}
+ ref={(el) => {
+ props.onRegisterMessage(el, messageID)
+ onCleanup(() => props.onUnregisterMessage(messageID))
+ }}
+ classList={{
+ "min-w-0 w-full max-w-full": true,
+ "md:max-w-200 2xl:max-w-[1000px]": props.centered,
+ }}
+ >
+ <Show when={commentCount() > 0}>
+ <div class="w-full px-4 md:px-5 pb-2">
+ <div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
+ <div class="flex w-max min-w-full justify-end gap-2">
+ <Index each={comments()}>
+ {(commentAccessor: () => MessageComment) => {
+ const comment = createMemo(() => commentAccessor())
+ return (
+ <div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
+ <div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
+ <FileIcon
+ node={{ path: comment().path, type: "file" }}
+ class="size-3.5 shrink-0"
+ />
+ <span class="truncate">{getFilename(comment().path)}</span>
+ <Show when={comment().selection}>
+ {(selection) => (
+ <span class="shrink-0 text-text-weak">
+ {selection().startLine === selection().endLine
+ ? `:${selection().startLine}`
+ : `:${selection().startLine}-${selection().endLine}`}
+ </span>
+ )}
+ </Show>
+ </div>
+ <div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words">
+ {comment().comment}
+ </div>
</div>
- </div>
- )
- }}
- </Index>
+ )
+ }}
+ </Index>
+ </div>
</div>
</div>
- </div>
- </Show>
- <SessionTurn
- sessionID={sessionID() ?? ""}
- messageID={messageID}
- active={active()}
- queued={queued()}
- status={active() ? sessionStatus() : undefined}
- showReasoningSummaries={settings.general.showReasoningSummaries()}
- shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
- editToolDefaultOpen={settings.general.editToolPartsExpanded()}
- classes={{
- root: "min-w-0 w-full relative",
- content: "flex flex-col justify-between !overflow-visible",
- container: "w-full px-4 md:px-5",
- }}
- />
- </div>
- )
- }}
- </For>
+ </Show>
+ <SessionTurn
+ sessionID={sessionID() ?? ""}
+ messageID={messageID}
+ active={active()}
+ queued={queued()}
+ status={active() ? sessionStatus() : undefined}
+ showReasoningSummaries={settings.general.showReasoningSummaries()}
+ shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
+ editToolDefaultOpen={settings.general.editToolPartsExpanded()}
+ classes={{
+ root: "min-w-0 w-full relative",
+ content: "flex flex-col justify-between !overflow-visible",
+ container: "w-full px-4 md:px-5",
+ }}
+ />
+ </div>
+ )
+ }}
+ </For>
+ </div>
</div>
</ScrollView>
</div>
diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx
index 49bed9490..cc4c17ee2 100644
--- a/packages/app/src/pages/session/terminal-panel.tsx
+++ b/packages/app/src/pages/session/terminal-panel.tsx
@@ -102,7 +102,7 @@ export function TerminalPanel() {
const all = createMemo(() => terminal.all())
const ids = createMemo(() => all().map((pty) => pty.id))
- const byId = createMemo(() => new Map(all().map((pty) => [pty.id, pty])))
+ const byId = createMemo(() => new Map(all().map((pty) => [pty.id, { ...pty }])))
const handleTerminalDragStart = (event: unknown) => {
const id = getDraggableId(event)
@@ -189,7 +189,13 @@ export function TerminalPanel() {
>
<Tabs.List class="h-10">
<SortableProvider ids={ids()}>
- <For each={all()}>{(pty) => <SortableTerminalTab terminal={pty} onClose={close} />}</For>
+ <For each={ids()}>
+ {(id) => (
+ <Show when={byId().get(id)}>
+ {(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />}
+ </Show>
+ )}
+ </For>
</SortableProvider>
<div class="h-full flex items-center justify-center">
<TooltipKeybind
diff --git a/packages/app/src/utils/notification-click.test.ts b/packages/app/src/utils/notification-click.test.ts
index 76535f83a..fa81b0e02 100644
--- a/packages/app/src/utils/notification-click.test.ts
+++ b/packages/app/src/utils/notification-click.test.ts
@@ -1,26 +1,27 @@
-import { describe, expect, test } from "bun:test"
-import { handleNotificationClick } from "./notification-click"
+import { afterEach, describe, expect, test } from "bun:test"
+import { handleNotificationClick, setNavigate } from "./notification-click"
describe("notification click", () => {
- test("focuses and navigates when href exists", () => {
+ afterEach(() => {
+ setNavigate(undefined as any)
+ })
+
+ test("navigates via registered navigate function", () => {
const calls: string[] = []
- handleNotificationClick("/abc/session/123", {
- focus: () => calls.push("focus"),
- location: {
- assign: (href) => calls.push(href),
- },
- })
- expect(calls).toEqual(["focus", "/abc/session/123"])
+ setNavigate((href) => calls.push(href))
+ handleNotificationClick("/abc/session/123")
+ expect(calls).toEqual(["/abc/session/123"])
})
- test("only focuses when href is missing", () => {
+ test("does not navigate when href is missing", () => {
const calls: string[] = []
- handleNotificationClick(undefined, {
- focus: () => calls.push("focus"),
- location: {
- assign: (href) => calls.push(href),
- },
- })
- expect(calls).toEqual(["focus"])
+ setNavigate((href) => calls.push(href))
+ handleNotificationClick(undefined)
+ expect(calls).toEqual([])
+ })
+
+ test("falls back to location.assign without registered navigate", () => {
+ handleNotificationClick("/abc/session/123")
+ // falls back to window.location.assign — no error thrown
})
})
diff --git a/packages/app/src/utils/notification-click.ts b/packages/app/src/utils/notification-click.ts
index 1234cd1d6..94086c595 100644
--- a/packages/app/src/utils/notification-click.ts
+++ b/packages/app/src/utils/notification-click.ts
@@ -1,12 +1,12 @@
-type WindowTarget = {
- focus: () => void
- location: {
- assign: (href: string) => void
- }
+let nav: ((href: string) => void) | undefined
+
+export const setNavigate = (fn: (href: string) => void) => {
+ nav = fn
}
-export const handleNotificationClick = (href?: string, target: WindowTarget = window) => {
- target.focus()
+export const handleNotificationClick = (href?: string) => {
+ window.focus()
if (!href) return
- target.location.assign(href)
+ if (nav) nav(href)
+ else window.location.assign(href)
}
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index ad5813ced..fa264844c 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
- "version": "1.2.15",
+ "version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts
index e2803459e..95c50fbdb 100644
--- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts
+++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts
@@ -43,7 +43,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
...(isBedrock
? {
anthropic_version: "bedrock-2023-05-31",
- anthropic_beta: supports1m ? "context-1m-2025-08-07" : undefined,
+ anthropic_beta: supports1m ? ["context-1m-2025-08-07"] : undefined,
model: undefined,
stream: undefined,
}
diff --git a/packages/console/core/package.json b/packages/console/core/package.json
index 99ba32df4..60eb69b20 100644
--- a/packages/console/core/package.json
+++ b/packages/console/core/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
- "version": "1.2.15",
+ "version": "1.2.17",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/console/function/package.json b/packages/console/function/package.json
index 6cdf75243..cca7329af 100644
--- a/packages/console/function/package.json
+++ b/packages/console/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
- "version": "1.2.15",
+ "version": "1.2.17",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json
index 09344f7fa..ed42abc08 100644
--- a/packages/console/mail/package.json
+++ b/packages/console/mail/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
- "version": "1.2.15",
+ "version": "1.2.17",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
diff --git a/packages/desktop-electron/.gitignore b/packages/desktop-electron/.gitignore
new file mode 100644
index 000000000..ac9d8db96
--- /dev/null
+++ b/packages/desktop-electron/.gitignore
@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+out/
+
+resources/opencode-cli*
+resources/icons
diff --git a/packages/desktop-electron/AGENTS.md b/packages/desktop-electron/AGENTS.md
new file mode 100644
index 000000000..7805ea835
--- /dev/null
+++ b/packages/desktop-electron/AGENTS.md
@@ -0,0 +1,4 @@
+# Desktop package notes
+
+- Renderer process should only call `window.api` from `src/preload`.
+- Main process should register IPC handlers in `src/main/ipc.ts`.
diff --git a/packages/desktop-electron/README.md b/packages/desktop-electron/README.md
new file mode 100644
index 000000000..ebaf48822
--- /dev/null
+++ b/packages/desktop-electron/README.md
@@ -0,0 +1,32 @@
+# OpenCode Desktop
+
+Native OpenCode desktop app, built with Tauri v2.
+
+## Development
+
+From the repo root:
+
+```bash
+bun install
+bun run --cwd packages/desktop tauri dev
+```
+
+This starts the Vite dev server on http://localhost:1420 and opens the native window.
+
+If you only want the web dev server (no native shell):
+
+```bash
+bun run --cwd packages/desktop dev
+```
+
+## Build
+
+To create a production `dist/` and build the native app bundle:
+
+```bash
+bun run --cwd packages/desktop tauri build
+```
+
+## Prerequisites
+
+Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions.
diff --git a/packages/desktop-electron/electron-builder.config.ts b/packages/desktop-electron/electron-builder.config.ts
new file mode 100644
index 000000000..e6b4bcd2b
--- /dev/null
+++ b/packages/desktop-electron/electron-builder.config.ts
@@ -0,0 +1,97 @@
+import type { Configuration } from "electron-builder"
+
+const channel = (() => {
+ const raw = process.env.OPENCODE_CHANNEL
+ if (raw === "dev" || raw === "beta" || raw === "prod") return raw
+ return "dev"
+})()
+
+const getBase = (): Configuration => ({
+ artifactName: "opencode-electron-${os}-${arch}.${ext}",
+ directories: {
+ output: "dist",
+ buildResources: "resources",
+ },
+ files: ["out/**/*", "resources/**/*"],
+ extraResources: [
+ {
+ from: "resources/",
+ to: "",
+ filter: ["opencode-cli*"],
+ },
+ {
+ from: "native/",
+ to: "native/",
+ filter: ["index.js", "index.d.ts", "build/Release/mac_window.node", "swift-build/**"],
+ },
+ ],
+ mac: {
+ category: "public.app-category.developer-tools",
+ icon: `resources/icons/icon.icns`,
+ hardenedRuntime: true,
+ gatekeeperAssess: false,
+ entitlements: "resources/entitlements.plist",
+ entitlementsInherit: "resources/entitlements.plist",
+ notarize: true,
+ target: ["dmg", "zip"],
+ },
+ dmg: {
+ sign: true,
+ },
+ protocols: {
+ name: "OpenCode",
+ schemes: ["opencode"],
+ },
+ win: {
+ icon: `resources/icons/icon.ico`,
+ target: ["nsis"],
+ },
+ nsis: {
+ oneClick: false,
+ allowToChangeInstallationDirectory: true,
+ installerIcon: `resources/icons/icon.ico`,
+ installerHeaderIcon: `resources/icons/icon.ico`,
+ },
+ linux: {
+ icon: `resources/icons`,
+ category: "Development",
+ target: ["AppImage", "deb", "rpm"],
+ },
+})
+
+function getConfig() {
+ const base = getBase()
+
+ switch (channel) {
+ case "dev": {
+ return {
+ ...base,
+ appId: "ai.opencode.desktop.dev",
+ productName: "OpenCode Dev",
+ rpm: { packageName: "opencode-dev" },
+ }
+ }
+ case "beta": {
+ return {
+ ...base,
+ appId: "ai.opencode.desktop.beta",
+ productName: "OpenCode Beta",
+ protocols: { name: "OpenCode Beta", schemes: ["opencode"] },
+ publish: { provider: "github", owner: "anomalyco", repo: "opencode-beta", channel: "latest" },
+ rpm: { packageName: "opencode-beta" },
+ }
+ }
+ case "prod": {
+ return {
+ ...base,
+ appId: "ai.opencode.desktop",
+ productName: "OpenCode",
+ protocols: { name: "OpenCode", schemes: ["opencode"] },
+ publish: { provider: "github", owner: "anomalyco", repo: "opencode", channel: "latest" },
+ rpm: { packageName: "opencode" },
+ }
+ }
+ }
+}
+
+export default getConfig()
diff --git a/packages/desktop-electron/electron.vite.config.ts b/packages/desktop-electron/electron.vite.config.ts
new file mode 100644
index 000000000..80c1d6b70
--- /dev/null
+++ b/packages/desktop-electron/electron.vite.config.ts
@@ -0,0 +1,41 @@
+import { defineConfig } from "electron-vite"
+import appPlugin from "@opencode-ai/app/vite"
+
+const channel = (() => {
+ const raw = process.env.OPENCODE_CHANNEL
+ if (raw === "dev" || raw === "beta" || raw === "prod") return raw
+ return "dev"
+})()
+
+export default defineConfig({
+ main: {
+ define: {
+ "import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel),
+ },
+ build: {
+ rollupOptions: {
+ input: { index: "src/main/index.ts" },
+ },
+ },
+ },
+ preload: {
+ build: {
+ rollupOptions: {
+ input: { index: "src/preload/index.ts" },
+ },
+ },
+ },
+ renderer: {
+ plugins: [appPlugin],
+ publicDir: "../app/public",
+ root: "src/renderer",
+ build: {
+ rollupOptions: {
+ input: {
+ main: "src/renderer/index.html",
+ loading: "src/renderer/loading.html",
+ },
+ },
+ },
+ },
+})
diff --git a/packages/desktop-electron/icons/README.md b/packages/desktop-electron/icons/README.md
new file mode 100644
index 000000000..fa219a77e
--- /dev/null
+++ b/packages/desktop-electron/icons/README.md
@@ -0,0 +1,11 @@
+# Tauri Icons
+
+Here's the process I've been using to create icons:
+
+- Save source image as `app-icon.png` in `packages/desktop`
+- `cd` to `packages/desktop`
+- Run `bun tauri icon -o src-tauri/icons/{environment}`
+- Use [Image2Icon](https://img2icnsapp.com/)'s 'Big Sur Icon' preset to generate an `icon.icns` file and place it in the appropriate icons folder
+
+The Image2Icon step is necessary as the `icon.icns` generated by `app-icon.png` does not apply the shadow/padding expected by macOS,
+so app icons appear larger than expected.
diff --git a/packages/desktop-electron/icons/beta/128x128.png b/packages/desktop-electron/icons/beta/128x128.png
new file mode 100644
index 000000000..751e80f1f
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/128x128.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/[email protected] b/packages/desktop-electron/icons/beta/[email protected]
new file mode 100644
index 000000000..fe330df41
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/32x32.png b/packages/desktop-electron/icons/beta/32x32.png
new file mode 100644
index 000000000..2703048ee
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/32x32.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/64x64.png b/packages/desktop-electron/icons/beta/64x64.png
new file mode 100644
index 000000000..ecd7fe314
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/64x64.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/Square107x107Logo.png b/packages/desktop-electron/icons/beta/Square107x107Logo.png
new file mode 100644
index 000000000..e6ea73f4d
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/Square107x107Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/Square142x142Logo.png b/packages/desktop-electron/icons/beta/Square142x142Logo.png
new file mode 100644
index 000000000..74ae729c4
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/Square142x142Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/Square150x150Logo.png b/packages/desktop-electron/icons/beta/Square150x150Logo.png
new file mode 100644
index 000000000..0b109b8f4
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/Square150x150Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/Square284x284Logo.png b/packages/desktop-electron/icons/beta/Square284x284Logo.png
new file mode 100644
index 000000000..0261ded42
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/Square284x284Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/Square30x30Logo.png b/packages/desktop-electron/icons/beta/Square30x30Logo.png
new file mode 100644
index 000000000..34158f10a
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/Square30x30Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/Square310x310Logo.png b/packages/desktop-electron/icons/beta/Square310x310Logo.png
new file mode 100644
index 000000000..f18bfada4
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/Square310x310Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/Square44x44Logo.png b/packages/desktop-electron/icons/beta/Square44x44Logo.png
new file mode 100644
index 000000000..6d1cc06c0
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/Square44x44Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/Square71x71Logo.png b/packages/desktop-electron/icons/beta/Square71x71Logo.png
new file mode 100644
index 000000000..a26084dc2
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/Square71x71Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/Square89x89Logo.png b/packages/desktop-electron/icons/beta/Square89x89Logo.png
new file mode 100644
index 000000000..58b0eb605
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/Square89x89Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/StoreLogo.png b/packages/desktop-electron/icons/beta/StoreLogo.png
new file mode 100644
index 000000000..648fd2114
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/StoreLogo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml b/packages/desktop-electron/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..2ffbf24b6
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+ <background android:drawable="@color/ic_launcher_background"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..39d1dd0d5
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..84908e71c
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..a6b8cb616
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..6522e0fba
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..b3449bd4f
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..7aa97d827
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..82bc9d22a
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..6b031ce85
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..34859de5e
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..4cdb71d62
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..a64be6ada
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..2de3c2734
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..0ead28866
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..bdd174825
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..69f74758e
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/android/values/ic_launcher_background.xml b/packages/desktop-electron/icons/beta/android/values/ic_launcher_background.xml
new file mode 100644
index 000000000..ea9c223a6
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/android/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="ic_launcher_background">#fff</color>
+</resources> \ No newline at end of file
diff --git a/packages/desktop-electron/icons/beta/icon.icns b/packages/desktop-electron/icons/beta/icon.icns
new file mode 100644
index 000000000..f98de5da8
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/icon.icns
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/icon.ico b/packages/desktop-electron/icons/beta/icon.ico
new file mode 100644
index 000000000..df8588c8e
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/icon.ico
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/icon.png b/packages/desktop-electron/icons/beta/icon.png
new file mode 100644
index 000000000..531304956
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/icon.png
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..e8ebb28ef
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..50c8015de
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..50c8015de
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..6e290dbc6
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..4ef554b4d
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..b9ddfd47c
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..b9ddfd47c
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..052322d68
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..50c8015de
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..9317b2500
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..9317b2500
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..6b921a17e
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..b83131d64
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..6b921a17e
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..685004995
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..1ffceb752
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..81c4178c9
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/beta/ios/[email protected] b/packages/desktop-electron/icons/beta/ios/[email protected]
new file mode 100644
index 000000000..d5453adff
--- /dev/null
+++ b/packages/desktop-electron/icons/beta/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/128x128.png b/packages/desktop-electron/icons/dev/128x128.png
new file mode 100644
index 000000000..d7fc4db14
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/128x128.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/[email protected] b/packages/desktop-electron/icons/dev/[email protected]
new file mode 100644
index 000000000..591882306
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/32x32.png b/packages/desktop-electron/icons/dev/32x32.png
new file mode 100644
index 000000000..53925cc4f
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/32x32.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/64x64.png b/packages/desktop-electron/icons/dev/64x64.png
new file mode 100644
index 000000000..a88ef15c6
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/64x64.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/Square107x107Logo.png b/packages/desktop-electron/icons/dev/Square107x107Logo.png
new file mode 100644
index 000000000..0de29ec82
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/Square107x107Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/Square142x142Logo.png b/packages/desktop-electron/icons/dev/Square142x142Logo.png
new file mode 100644
index 000000000..af62e8e1e
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/Square142x142Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/Square150x150Logo.png b/packages/desktop-electron/icons/dev/Square150x150Logo.png
new file mode 100644
index 000000000..2b19dc39c
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/Square150x150Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/Square284x284Logo.png b/packages/desktop-electron/icons/dev/Square284x284Logo.png
new file mode 100644
index 000000000..eda6d9901
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/Square284x284Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/Square30x30Logo.png b/packages/desktop-electron/icons/dev/Square30x30Logo.png
new file mode 100644
index 000000000..dad821ba8
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/Square30x30Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/Square310x310Logo.png b/packages/desktop-electron/icons/dev/Square310x310Logo.png
new file mode 100644
index 000000000..555b3b197
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/Square310x310Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/Square44x44Logo.png b/packages/desktop-electron/icons/dev/Square44x44Logo.png
new file mode 100644
index 000000000..9f8ad001f
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/Square44x44Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/Square71x71Logo.png b/packages/desktop-electron/icons/dev/Square71x71Logo.png
new file mode 100644
index 000000000..43feb7848
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/Square71x71Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/Square89x89Logo.png b/packages/desktop-electron/icons/dev/Square89x89Logo.png
new file mode 100644
index 000000000..628cc597f
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/Square89x89Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/StoreLogo.png b/packages/desktop-electron/icons/dev/StoreLogo.png
new file mode 100644
index 000000000..8d3aa53cf
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/StoreLogo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml b/packages/desktop-electron/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..2ffbf24b6
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+ <background android:drawable="@color/ic_launcher_background"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..b355e37fe
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..c33f8713b
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..04e37aa65
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..98e53cd22
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..40fe6e378
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..4814f1ddf
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..608493283
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..898066a3f
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..64035c0f3
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f47691bf4
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..dba6f5635
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..764702604
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..2e8430a60
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..db953d128
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..d5c9ba6a8
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/android/values/ic_launcher_background.xml b/packages/desktop-electron/icons/dev/android/values/ic_launcher_background.xml
new file mode 100644
index 000000000..ea9c223a6
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/android/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="ic_launcher_background">#fff</color>
+</resources> \ No newline at end of file
diff --git a/packages/desktop-electron/icons/dev/icon.icns b/packages/desktop-electron/icons/dev/icon.icns
new file mode 100644
index 000000000..d73a94904
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/icon.icns
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/icon.ico b/packages/desktop-electron/icons/dev/icon.ico
new file mode 100644
index 000000000..bec385d9a
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/icon.ico
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/icon.png b/packages/desktop-electron/icons/dev/icon.png
new file mode 100644
index 000000000..6de37ea29
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/icon.png
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..0e823043e
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..54e4b2aac
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..54e4b2aac
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..645b01561
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..054225c6e
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..0b1b2e0b7
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..0b1b2e0b7
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..d2c42592b
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..54e4b2aac
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..471ed2eec
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..471ed2eec
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..1a490cbf1
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..f53b404e5
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..1a490cbf1
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..bdc759eef
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..d22096a2d
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..d675773d1
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/dev/ios/[email protected] b/packages/desktop-electron/icons/dev/ios/[email protected]
new file mode 100644
index 000000000..31698afce
--- /dev/null
+++ b/packages/desktop-electron/icons/dev/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/128x128.png b/packages/desktop-electron/icons/prod/128x128.png
new file mode 100644
index 000000000..caf7b02eb
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/128x128.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/[email protected] b/packages/desktop-electron/icons/prod/[email protected]
new file mode 100644
index 000000000..47fe4c61e
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/32x32.png b/packages/desktop-electron/icons/prod/32x32.png
new file mode 100644
index 000000000..5868bcc93
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/32x32.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/64x64.png b/packages/desktop-electron/icons/prod/64x64.png
new file mode 100644
index 000000000..1ed7425d8
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/64x64.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/Square107x107Logo.png b/packages/desktop-electron/icons/prod/Square107x107Logo.png
new file mode 100644
index 000000000..1db249bf7
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/Square107x107Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/Square142x142Logo.png b/packages/desktop-electron/icons/prod/Square142x142Logo.png
new file mode 100644
index 000000000..1961c3408
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/Square142x142Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/Square150x150Logo.png b/packages/desktop-electron/icons/prod/Square150x150Logo.png
new file mode 100644
index 000000000..abc507347
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/Square150x150Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/Square284x284Logo.png b/packages/desktop-electron/icons/prod/Square284x284Logo.png
new file mode 100644
index 000000000..51e2a1b9f
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/Square284x284Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/Square30x30Logo.png b/packages/desktop-electron/icons/prod/Square30x30Logo.png
new file mode 100644
index 000000000..066a1fd0c
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/Square30x30Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/Square310x310Logo.png b/packages/desktop-electron/icons/prod/Square310x310Logo.png
new file mode 100644
index 000000000..2a85c8e95
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/Square310x310Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/Square44x44Logo.png b/packages/desktop-electron/icons/prod/Square44x44Logo.png
new file mode 100644
index 000000000..c855b8063
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/Square44x44Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/Square71x71Logo.png b/packages/desktop-electron/icons/prod/Square71x71Logo.png
new file mode 100644
index 000000000..c8168f711
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/Square71x71Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/Square89x89Logo.png b/packages/desktop-electron/icons/prod/Square89x89Logo.png
new file mode 100644
index 000000000..19ec1777d
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/Square89x89Logo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/StoreLogo.png b/packages/desktop-electron/icons/prod/StoreLogo.png
new file mode 100644
index 000000000..3fd053d34
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/StoreLogo.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml b/packages/desktop-electron/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..2ffbf24b6
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+ <background android:drawable="@color/ic_launcher_background"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..4f3ea0e36
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..7db80699b
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..a54ebe652
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..9337ccfa3
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..0bfc1082e
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..5b02ec732
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..322aeaeaa
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..ca1e336cc
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..f71110799
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..287a6b500
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..9d3d06a86
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..d4b6fde1b
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..bde8d7596
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..03df7809d
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..62363be04
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/android/values/ic_launcher_background.xml b/packages/desktop-electron/icons/prod/android/values/ic_launcher_background.xml
new file mode 100644
index 000000000..ea9c223a6
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/android/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="ic_launcher_background">#fff</color>
+</resources> \ No newline at end of file
diff --git a/packages/desktop-electron/icons/prod/icon.icns b/packages/desktop-electron/icons/prod/icon.icns
new file mode 100644
index 000000000..be910ad5f
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/icon.icns
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/icon.ico b/packages/desktop-electron/icons/prod/icon.ico
new file mode 100644
index 000000000..ff88d21e4
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/icon.ico
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/icon.png b/packages/desktop-electron/icons/prod/icon.png
new file mode 100644
index 000000000..0ecbb6d5f
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/icon.png
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..eb137e164
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..aa76ab10b
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..aa76ab10b
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..c58ea3d49
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..0eeb4d9bf
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..32601c70a
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..32601c70a
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..a372c4a11
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..aa76ab10b
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..e82ce2765
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..e82ce2765
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..15ad59362
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..2260671c0
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..15ad59362
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..5c66bd3b1
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..a5b05f3b5
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..9c0615d41
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/icons/prod/ios/[email protected] b/packages/desktop-electron/icons/prod/ios/[email protected]
new file mode 100644
index 000000000..6b792b36a
--- /dev/null
+++ b/packages/desktop-electron/icons/prod/ios/[email protected]
Binary files differ
diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json
new file mode 100644
index 000000000..9f635ebbd
--- /dev/null
+++ b/packages/desktop-electron/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@opencode-ai/desktop-electron",
+ "private": true,
+ "version": "1.2.17",
+ "type": "module",
+ "license": "MIT",
+ "homepage": "https://opencode.ai",
+ "author": {
+ "name": "OpenCode",
+ "email": "[email protected]"
+ },
+ "scripts": {
+ "typecheck": "tsgo -b",
+ "predev": "bun ./scripts/predev.ts",
+ "dev": "electron-vite dev",
+ "prebuild": "bun ./scripts/copy-icons.ts",
+ "build": "electron-vite build",
+ "preview": "electron-vite preview",
+ "package": "electron-builder --config electron-builder.config.ts",
+ "package:mac": "electron-builder --mac --config electron-builder.config.ts",
+ "package:win": "electron-builder --win --config electron-builder.config.ts",
+ "package:linux": "electron-builder --linux --config electron-builder.config.ts",
+ "native:build": "bun install --cwd native"
+ },
+ "main": "./out/main/index.js",
+ "dependencies": {
+ "@opencode-ai/app": "workspace:*",
+ "@opencode-ai/ui": "workspace:*",
+ "@solid-primitives/i18n": "2.2.1",
+ "@solid-primitives/storage": "catalog:",
+ "@solidjs/meta": "catalog:",
+ "@solidjs/router": "0.15.4",
+ "electron-log": "^5",
+ "electron-store": "^10",
+ "electron-updater": "^6",
+ "electron-window-state": "^5.0.3",
+ "marked": "^15",
+ "solid-js": "catalog:",
+ "tree-kill": "^1.2.2"
+ },
+ "devDependencies": {
+ "@actions/artifact": "4.0.0",
+ "@types/bun": "catalog:",
+ "@types/node": "catalog:",
+ "@typescript/native-preview": "catalog:",
+ "electron": "40.4.1",
+ "electron-builder": "^26",
+ "electron-vite": "^5",
+ "typescript": "~5.6.2",
+ "vite": "catalog:"
+ }
+}
diff --git a/packages/desktop-electron/resources/entitlements.plist b/packages/desktop-electron/resources/entitlements.plist
new file mode 100644
index 000000000..61d6c38ce
--- /dev/null
+++ b/packages/desktop-electron/resources/entitlements.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.cs.allow-jit</key>
+ <true/>
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
+ <true/>
+ <key>com.apple.security.cs.disable-executable-page-protection</key>
+ <true/>
+ <key>com.apple.security.cs.allow-dyld-environment-variables</key>
+ <true/>
+ <key>com.apple.security.cs.disable-library-validation</key>
+ <true/>
+ <key>com.apple.security.automation.apple-events</key>
+ <true/>
+ <key>com.apple.security.device.audio-input</key>
+ <true/>
+ <key>com.apple.security.device.camera</key>
+ <true/>
+ <key>com.apple.security.personal-information.addressbook</key>
+ <true/>
+ <key>com.apple.security.personal-information.calendars</key>
+ <true/>
+ <key>com.apple.security.personal-information.location</key>
+ <true/>
+ <key>com.apple.security.personal-information.photos-library</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/desktop-electron/scripts/copy-bundles.ts b/packages/desktop-electron/scripts/copy-bundles.ts
new file mode 100644
index 000000000..6ef3335eb
--- /dev/null
+++ b/packages/desktop-electron/scripts/copy-bundles.ts
@@ -0,0 +1,12 @@
+import { $ } from "bun"
+import * as path from "node:path"
+
+import { RUST_TARGET } from "./utils"
+
+if (!RUST_TARGET) throw new Error("RUST_TARGET not defined")
+
+const BUNDLE_DIR = "dist"
+const BUNDLES_OUT_DIR = path.join(process.cwd(), "dist/bundles")
+
+await $`mkdir -p ${BUNDLES_OUT_DIR}`
+await $`cp -r ${BUNDLE_DIR}/* ${BUNDLES_OUT_DIR}`
diff --git a/packages/desktop-electron/scripts/copy-icons.ts b/packages/desktop-electron/scripts/copy-icons.ts
new file mode 100644
index 000000000..400f42757
--- /dev/null
+++ b/packages/desktop-electron/scripts/copy-icons.ts
@@ -0,0 +1,12 @@
+import { $ } from "bun"
+import { resolveChannel } from "./utils"
+
+const arg = process.argv[2]
+const channel = arg === "dev" || arg === "beta" || arg === "prod" ? arg : resolveChannel()
+
+const src = `./icons/${channel}`
+const dest = "resources/icons"
+
+await $`rm -rf ${dest}`
+await $`cp -R ${src} ${dest}`
+console.log(`Copied ${channel} icons from ${src} to ${dest}`)
diff --git a/packages/desktop-electron/scripts/finalize-latest-yml.ts b/packages/desktop-electron/scripts/finalize-latest-yml.ts
new file mode 100644
index 000000000..42ec23b64
--- /dev/null
+++ b/packages/desktop-electron/scripts/finalize-latest-yml.ts
@@ -0,0 +1,116 @@
+#!/usr/bin/env bun
+
+import { $ } from "bun"
+import path from "path"
+
+const dir = process.env.LATEST_YML_DIR!
+if (!dir) throw new Error("LATEST_YML_DIR is required")
+
+const repo = process.env.GH_REPO
+if (!repo) throw new Error("GH_REPO is required")
+
+const version = process.env.OPENCODE_VERSION
+if (!version) throw new Error("OPENCODE_VERSION is required")
+
+type FileEntry = {
+ url: string
+ sha512: string
+ size: number
+ blockMapSize?: number
+}
+
+type LatestYml = {
+ version: string
+ files: FileEntry[]
+ releaseDate: string
+}
+
+function parse(content: string): LatestYml {
+ const lines = content.split("\n")
+ let version = ""
+ let releaseDate = ""
+ const files: FileEntry[] = []
+ let current: Partial<FileEntry> | undefined
+
+ const flush = () => {
+ if (current?.url && current.sha512 && current.size) files.push(current as FileEntry)
+ current = undefined
+ }
+
+ for (const line of lines) {
+ const indented = line.startsWith(" ") || line.startsWith(" -")
+ if (line.startsWith("version:")) version = line.slice("version:".length).trim()
+ else if (line.startsWith("releaseDate:"))
+ releaseDate = line.slice("releaseDate:".length).trim().replace(/^'|'$/g, "")
+ else if (line.trim().startsWith("- url:")) {
+ flush()
+ current = { url: line.trim().slice("- url:".length).trim() }
+ } else if (indented && current && line.trim().startsWith("sha512:"))
+ current.sha512 = line.trim().slice("sha512:".length).trim()
+ else if (indented && current && line.trim().startsWith("size:"))
+ current.size = Number(line.trim().slice("size:".length).trim())
+ else if (indented && current && line.trim().startsWith("blockMapSize:"))
+ current.blockMapSize = Number(line.trim().slice("blockMapSize:".length).trim())
+ else if (!indented && current) flush()
+ }
+ flush()
+
+ return { version, files, releaseDate }
+}
+
+function serialize(data: LatestYml) {
+ const lines = [`version: ${data.version}`, "files:"]
+ for (const file of data.files) {
+ lines.push(` - url: ${file.url}`)
+ lines.push(` sha512: ${file.sha512}`)
+ lines.push(` size: ${file.size}`)
+ if (file.blockMapSize) lines.push(` blockMapSize: ${file.blockMapSize}`)
+ }
+ lines.push(`releaseDate: '${data.releaseDate}'`)
+ return lines.join("\n") + "\n"
+}
+
+async function read(subdir: string, filename: string): Promise<LatestYml | undefined> {
+ const file = Bun.file(path.join(dir, subdir, filename))
+ if (!(await file.exists())) return undefined
+ return parse(await file.text())
+}
+
+const output: Record<string, string> = {}
+
+// Windows: single arch, pass through
+const win = await read("latest-yml-x86_64-pc-windows-msvc", "latest.yml")
+if (win) output["latest.yml"] = serialize(win)
+
+// Linux x64: pass through
+const linuxX64 = await read("latest-yml-x86_64-unknown-linux-gnu", "latest-linux.yml")
+if (linuxX64) output["latest-linux.yml"] = serialize(linuxX64)
+
+// Linux arm64: pass through
+const linuxArm64 = await read("latest-yml-aarch64-unknown-linux-gnu", "latest-linux-arm64.yml")
+if (linuxArm64) output["latest-linux-arm64.yml"] = serialize(linuxArm64)
+
+// macOS: merge arm64 + x64 into single file
+const macX64 = await read("latest-yml-x86_64-apple-darwin", "latest-mac.yml")
+const macArm64 = await read("latest-yml-aarch64-apple-darwin", "latest-mac.yml")
+if (macX64 || macArm64) {
+ const base = macArm64 ?? macX64!
+ output["latest-mac.yml"] = serialize({
+ version: base.version,
+ files: [...(macArm64?.files ?? []), ...(macX64?.files ?? [])],
+ releaseDate: base.releaseDate,
+ })
+}
+
+// Upload to release
+const tag = `v${version}`
+const tmp = process.env.RUNNER_TEMP ?? "/tmp"
+
+for (const [filename, content] of Object.entries(output)) {
+ const filepath = path.join(tmp, filename)
+ await Bun.write(filepath, content)
+ await $`gh release upload ${tag} ${filepath} --clobber --repo ${repo}`
+ console.log(`uploaded ${filename}`)
+}
+
+console.log("finalized latest yml files")
diff --git a/packages/desktop-electron/scripts/predev.ts b/packages/desktop-electron/scripts/predev.ts
new file mode 100644
index 000000000..a688d0e7f
--- /dev/null
+++ b/packages/desktop-electron/scripts/predev.ts
@@ -0,0 +1,17 @@
+import { $ } from "bun"
+
+import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils"
+
+await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}`
+
+const RUST_TARGET = Bun.env.RUST_TARGET
+
+const sidecarConfig = getCurrentSidecar(RUST_TARGET)
+
+const binaryPath = windowsify(`../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode`)
+
+await (sidecarConfig.ocBinary.includes("-baseline")
+ ? $`cd ../opencode && bun run build --single --baseline`
+ : $`cd ../opencode && bun run build --single`)
+
+await copyBinaryToSidecarFolder(binaryPath, RUST_TARGET)
diff --git a/packages/desktop-electron/scripts/prepare.ts b/packages/desktop-electron/scripts/prepare.ts
new file mode 100755
index 000000000..3764db921
--- /dev/null
+++ b/packages/desktop-electron/scripts/prepare.ts
@@ -0,0 +1,24 @@
+#!/usr/bin/env bun
+import { $ } from "bun"
+
+import { Script } from "@opencode-ai/script"
+import { copyBinaryToSidecarFolder, getCurrentSidecar, resolveChannel, windowsify } from "./utils"
+
+const channel = resolveChannel()
+await $`bun ./scripts/copy-icons.ts ${channel}`
+
+const pkg = await Bun.file("./package.json").json()
+pkg.version = Script.version
+await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
+console.log(`Updated package.json version to ${Script.version}`)
+
+const sidecarConfig = getCurrentSidecar()
+
+const dir = "resources/opencode-binaries"
+
+await $`mkdir -p ${dir}`
+await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
+
+await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
+
+await $`rm -rf ${dir}`
diff --git a/packages/desktop-electron/scripts/utils.ts b/packages/desktop-electron/scripts/utils.ts
new file mode 100644
index 000000000..4c9af1fc7
--- /dev/null
+++ b/packages/desktop-electron/scripts/utils.ts
@@ -0,0 +1,69 @@
+import { $ } from "bun"
+
+export type Channel = "dev" | "beta" | "prod"
+
+export function resolveChannel(): Channel {
+ const raw = Bun.env.OPENCODE_CHANNEL
+ if (raw === "dev" || raw === "beta" || raw === "prod") return raw
+ return "dev"
+}
+
+export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; assetExt: string }> = [
+ {
+ rustTarget: "aarch64-apple-darwin",
+ ocBinary: "opencode-darwin-arm64",
+ assetExt: "zip",
+ },
+ {
+ rustTarget: "x86_64-apple-darwin",
+ ocBinary: "opencode-darwin-x64-baseline",
+ assetExt: "zip",
+ },
+ {
+ rustTarget: "x86_64-pc-windows-msvc",
+ ocBinary: "opencode-windows-x64-baseline",
+ assetExt: "zip",
+ },
+ {
+ rustTarget: "x86_64-unknown-linux-gnu",
+ ocBinary: "opencode-linux-x64-baseline",
+ assetExt: "tar.gz",
+ },
+ {
+ rustTarget: "aarch64-unknown-linux-gnu",
+ ocBinary: "opencode-linux-arm64",
+ assetExt: "tar.gz",
+ },
+]
+
+export const RUST_TARGET = Bun.env.RUST_TARGET
+
+function nativeTarget() {
+ const { platform, arch } = process
+ if (platform === "darwin") return arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin"
+ if (platform === "win32") return "x86_64-pc-windows-msvc"
+ if (platform === "linux") return arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu"
+ throw new Error(`Unsupported platform: ${platform}/${arch}`)
+}
+
+export function getCurrentSidecar(target = RUST_TARGET ?? nativeTarget()) {
+ const binaryConfig = SIDECAR_BINARIES.find((b) => b.rustTarget === target)
+ if (!binaryConfig) throw new Error(`Sidecar configuration not available for Rust target '${target}'`)
+
+ return binaryConfig
+}
+
+export async function copyBinaryToSidecarFolder(source: string) {
+ const dir = `resources`
+ await $`mkdir -p ${dir}`
+ const dest = windowsify(`${dir}/opencode-cli`)
+ await $`cp ${source} ${dest}`
+ if (process.platform === "darwin") await $`codesign --force --sign - ${dest}`
+
+ console.log(`Copied ${source} to ${dest}`)
+}
+
+export function windowsify(path: string) {
+ if (path.endsWith(".exe")) return path
+ return `${path}${process.platform === "win32" ? ".exe" : ""}`
+}
diff --git a/packages/desktop-electron/src/main/apps.ts b/packages/desktop-electron/src/main/apps.ts
new file mode 100644
index 000000000..2b4603789
--- /dev/null
+++ b/packages/desktop-electron/src/main/apps.ts
@@ -0,0 +1,148 @@
+import { execFileSync } from "node:child_process"
+import { existsSync, readFileSync, readdirSync } from "node:fs"
+import { dirname, extname, join } from "node:path"
+
+export function checkAppExists(appName: string): boolean {
+ if (process.platform === "win32") return true
+ if (process.platform === "linux") return true
+ return checkMacosApp(appName)
+}
+
+export function resolveAppPath(appName: string): string | null {
+ if (process.platform !== "win32") return appName
+ return resolveWindowsAppPath(appName)
+}
+
+export function wslPath(path: string, mode: "windows" | "linux" | null): string {
+ if (process.platform !== "win32") return path
+
+ const flag = mode === "windows" ? "-w" : "-u"
+ try {
+ if (path.startsWith("~")) {
+ const suffix = path.slice(1)
+ const cmd = `wslpath ${flag} \"$HOME${suffix.replace(/\"/g, '\\"')}\"`
+ const output = execFileSync("wsl", ["-e", "sh", "-lc", cmd])
+ return output.toString().trim()
+ }
+
+ const output = execFileSync("wsl", ["-e", "wslpath", flag, path])
+ return output.toString().trim()
+ } catch (error) {
+ throw new Error(`Failed to run wslpath: ${String(error)}`)
+ }
+}
+
+function checkMacosApp(appName: string) {
+ const locations = [`/Applications/${appName}.app`, `/System/Applications/${appName}.app`]
+
+ const home = process.env.HOME
+ if (home) locations.push(`${home}/Applications/${appName}.app`)
+
+ if (locations.some((location) => existsSync(location))) return true
+
+ try {
+ execFileSync("which", [appName])
+ return true
+ } catch {
+ return false
+ }
+}
+
+function resolveWindowsAppPath(appName: string): string | null {
+ let output: string
+ try {
+ output = execFileSync("where", [appName]).toString()
+ } catch {
+ return null
+ }
+
+ const paths = output
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter((line) => line.length > 0)
+
+ const hasExt = (path: string, ext: string) => extname(path).toLowerCase() === `.${ext}`
+
+ const exe = paths.find((path) => hasExt(path, "exe"))
+ if (exe) return exe
+
+ const resolveCmd = (path: string) => {
+ const content = readFileSync(path, "utf8")
+ for (const token of content.split('"').map((value: string) => value.trim())) {
+ const lower = token.toLowerCase()
+ if (!lower.includes(".exe")) continue
+
+ const index = lower.indexOf("%~dp0")
+ if (index >= 0) {
+ const base = dirname(path)
+ const suffix = token.slice(index + 5)
+ const resolved = suffix
+ .replace(/\//g, "\\")
+ .split("\\")
+ .filter((part: string) => part && part !== ".")
+ .reduce((current: string, part: string) => {
+ if (part === "..") return dirname(current)
+ return join(current, part)
+ }, base)
+
+ if (existsSync(resolved)) return resolved
+ }
+
+ if (existsSync(token)) return token
+ }
+
+ return null
+ }
+
+ for (const path of paths) {
+ if (hasExt(path, "cmd") || hasExt(path, "bat")) {
+ const resolved = resolveCmd(path)
+ if (resolved) return resolved
+ }
+
+ if (!extname(path)) {
+ const cmd = `${path}.cmd`
+ if (existsSync(cmd)) {
+ const resolved = resolveCmd(cmd)
+ if (resolved) return resolved
+ }
+
+ const bat = `${path}.bat`
+ if (existsSync(bat)) {
+ const resolved = resolveCmd(bat)
+ if (resolved) return resolved
+ }
+ }
+ }
+
+ const key = appName
+ .split("")
+ .filter((value: string) => /[a-z0-9]/i.test(value))
+ .map((value: string) => value.toLowerCase())
+ .join("")
+
+ if (key) {
+ for (const path of paths) {
+ const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))]
+ for (const dir of dirs) {
+ try {
+ for (const entry of readdirSync(dir)) {
+ const candidate = join(dir, entry)
+ if (!hasExt(candidate, "exe")) continue
+ const stem = entry.replace(/\.exe$/i, "")
+ const name = stem
+ .split("")
+ .filter((value: string) => /[a-z0-9]/i.test(value))
+ .map((value: string) => value.toLowerCase())
+ .join("")
+ if (name.includes(key) || key.includes(name)) return candidate
+ }
+ } catch {
+ continue
+ }
+ }
+ }
+ }
+
+ return paths[0] ?? null
+}
diff --git a/packages/desktop-electron/src/main/cli.ts b/packages/desktop-electron/src/main/cli.ts
new file mode 100644
index 000000000..e338d3913
--- /dev/null
+++ b/packages/desktop-electron/src/main/cli.ts
@@ -0,0 +1,279 @@
+import { execFileSync, spawn } from "node:child_process"
+import { EventEmitter } from "node:events"
+import { chmodSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"
+import { tmpdir } from "node:os"
+import { dirname, join } from "node:path"
+import readline from "node:readline"
+import { fileURLToPath } from "node:url"
+import { app } from "electron"
+import treeKill from "tree-kill"
+
+import { WSL_ENABLED_KEY } from "./constants"
+import { store } from "./store"
+
+const CLI_INSTALL_DIR = ".opencode/bin"
+const CLI_BINARY_NAME = "opencode"
+
+export type ServerConfig = {
+ hostname?: string
+ port?: number
+}
+
+export type Config = {
+ server?: ServerConfig
+}
+
+export type TerminatedPayload = { code: number | null; signal: number | null }
+
+export type CommandEvent =
+ | { type: "stdout"; value: string }
+ | { type: "stderr"; value: string }
+ | { type: "error"; value: string }
+ | { type: "terminated"; value: TerminatedPayload }
+ | { type: "sqlite"; value: SqliteMigrationProgress }
+
+export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
+
+export type CommandChild = {
+ kill: () => void
+}
+
+const root = dirname(fileURLToPath(import.meta.url))
+
+export function getSidecarPath() {
+ const suffix = process.platform === "win32" ? ".exe" : ""
+ const path = app.isPackaged
+ ? join(process.resourcesPath, `opencode-cli${suffix}`)
+ : join(root, "../../resources", `opencode-cli${suffix}`)
+ console.log(`[cli] Sidecar path resolved: ${path} (isPackaged: ${app.isPackaged})`)
+ return path
+}
+
+export async function getConfig(): Promise<Config | null> {
+ const { events } = spawnCommand("debug config", {})
+ let output = ""
+
+ await new Promise<void>((resolve) => {
+ events.on("stdout", (line: string) => {
+ output += line
+ })
+ events.on("stderr", (line: string) => {
+ output += line
+ })
+ events.on("terminated", () => resolve())
+ events.on("error", () => resolve())
+ })
+
+ try {
+ return JSON.parse(output) as Config
+ } catch {
+ return null
+ }
+}
+
+export async function installCli(): Promise<string> {
+ if (process.platform === "win32") {
+ throw new Error("CLI installation is only supported on macOS & Linux")
+ }
+
+ const sidecar = getSidecarPath()
+ const scriptPath = join(app.getAppPath(), "install")
+ const script = readFileSync(scriptPath, "utf8")
+ const tempScript = join(tmpdir(), "opencode-install.sh")
+
+ writeFileSync(tempScript, script, "utf8")
+ chmodSync(tempScript, 0o755)
+
+ const cmd = spawn(tempScript, ["--binary", sidecar], { stdio: "pipe" })
+ return await new Promise<string>((resolve, reject) => {
+ cmd.on("exit", (code: number | null) => {
+ try {
+ unlinkSync(tempScript)
+ } catch {}
+ if (code === 0) {
+ const installPath = getCliInstallPath()
+ if (installPath) return resolve(installPath)
+ return reject(new Error("Could not determine install path"))
+ }
+ reject(new Error("Install script failed"))
+ })
+ })
+}
+
+export function syncCli() {
+ if (!app.isPackaged) return
+ const installPath = getCliInstallPath()
+ if (!installPath) return
+
+ let version = ""
+ try {
+ version = execFileSync(installPath, ["--version"]).toString().trim()
+ } catch {
+ return
+ }
+
+ const cli = parseVersion(version)
+ const appVersion = parseVersion(app.getVersion())
+ if (!cli || !appVersion) return
+ if (compareVersions(cli, appVersion) >= 0) return
+ void installCli().catch(() => undefined)
+}
+
+export function serve(hostname: string, port: number, password: string) {
+ const args = `--print-logs --log-level WARN serve --hostname ${hostname} --port ${port}`
+ const env = {
+ OPENCODE_SERVER_USERNAME: "opencode",
+ OPENCODE_SERVER_PASSWORD: password,
+ }
+
+ return spawnCommand(args, env)
+}
+
+export function spawnCommand(args: string, extraEnv: Record<string, string>) {
+ console.log(`[cli] Spawning command with args: ${args}`)
+ const base = Object.fromEntries(
+ Object.entries(process.env).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
+ )
+ const envs = {
+ ...base,
+ OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
+ OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
+ OPENCODE_CLIENT: "desktop",
+ XDG_STATE_HOME: app.getPath("userData"),
+ ...extraEnv,
+ }
+
+ const { cmd, cmdArgs } = buildCommand(args, envs)
+ console.log(`[cli] Executing: ${cmd} ${cmdArgs.join(" ")}`)
+ const child = spawn(cmd, cmdArgs, {
+ env: envs,
+ detached: true,
+ windowsHide: true,
+ stdio: ["ignore", "pipe", "pipe"],
+ })
+ console.log(`[cli] Spawned process with PID: ${child.pid}`)
+
+ const events = new EventEmitter()
+ const exit = new Promise<TerminatedPayload>((resolve) => {
+ child.on("exit", (code: number | null, signal: NodeJS.Signals | null) => {
+ console.log(`[cli] Process exited with code: ${code}, signal: ${signal}`)
+ resolve({ code: code ?? null, signal: null })
+ })
+ child.on("error", (error: Error) => {
+ console.error(`[cli] Process error: ${error.message}`)
+ events.emit("error", error.message)
+ })
+ })
+
+ const stdout = child.stdout
+ const stderr = child.stderr
+
+ if (stdout) {
+ readline.createInterface({ input: stdout }).on("line", (line: string) => {
+ if (handleSqliteProgress(events, line)) return
+ events.emit("stdout", `${line}\n`)
+ })
+ }
+
+ if (stderr) {
+ readline.createInterface({ input: stderr }).on("line", (line: string) => {
+ if (handleSqliteProgress(events, line)) return
+ events.emit("stderr", `${line}\n`)
+ })
+ }
+
+ exit.then((payload) => {
+ events.emit("terminated", payload)
+ })
+
+ const kill = () => {
+ if (!child.pid) return
+ treeKill(child.pid)
+ }
+
+ return { events, child: { kill }, exit }
+}
+
+function handleSqliteProgress(events: EventEmitter, line: string) {
+ const stripped = line.startsWith("sqlite-migration:") ? line.slice("sqlite-migration:".length).trim() : null
+ if (!stripped) return false
+ if (stripped === "done") {
+ events.emit("sqlite", { type: "Done" })
+ return true
+ }
+ const value = Number.parseInt(stripped, 10)
+ if (!Number.isNaN(value)) {
+ events.emit("sqlite", { type: "InProgress", value })
+ return true
+ }
+ return false
+}
+
+function buildCommand(args: string, env: Record<string, string>) {
+ if (process.platform === "win32" && isWslEnabled()) {
+ console.log(`[cli] Using WSL mode`)
+ const version = app.getVersion()
+ const script = [
+ "set -e",
+ 'BIN="$HOME/.opencode/bin/opencode"',
+ 'if [ ! -x "$BIN" ]; then',
+ ` curl -fsSL https://opencode.ai/install | bash -s -- --version ${shellEscape(version)} --no-modify-path`,
+ "fi",
+ `${envPrefix(env)} exec "$BIN" ${args}`,
+ ].join("\n")
+
+ return { cmd: "wsl", cmdArgs: ["-e", "bash", "-lc", script] }
+ }
+
+ if (process.platform === "win32") {
+ const sidecar = getSidecarPath()
+ console.log(`[cli] Windows direct mode, sidecar: ${sidecar}`)
+ return { cmd: sidecar, cmdArgs: args.split(" ") }
+ }
+
+ const sidecar = getSidecarPath()
+ const shell = process.env.SHELL || "/bin/sh"
+ const line = shell.endsWith("/nu") ? `^\"${sidecar}\" ${args}` : `\"${sidecar}\" ${args}`
+ console.log(`[cli] Unix mode, shell: ${shell}, command: ${line}`)
+ return { cmd: shell, cmdArgs: ["-l", "-c", line] }
+}
+
+function envPrefix(env: Record<string, string>) {
+ const entries = Object.entries(env).map(([key, value]) => `${key}=${shellEscape(value)}`)
+ return entries.join(" ")
+}
+
+function shellEscape(input: string) {
+ if (!input) return "''"
+ return `'${input.replace(/'/g, `'"'"'`)}'`
+}
+
+function getCliInstallPath() {
+ const home = process.env.HOME
+ if (!home) return null
+ return join(home, CLI_INSTALL_DIR, CLI_BINARY_NAME)
+}
+
+function isWslEnabled() {
+ return store.get(WSL_ENABLED_KEY) === true
+}
+
+function parseVersion(value: string) {
+ const parts = value
+ .replace(/^v/, "")
+ .split(".")
+ .map((part) => Number.parseInt(part, 10))
+ if (parts.some((part) => Number.isNaN(part))) return null
+ return parts
+}
+
+function compareVersions(a: number[], b: number[]) {
+ const len = Math.max(a.length, b.length)
+ for (let i = 0; i < len; i += 1) {
+ const left = a[i] ?? 0
+ const right = b[i] ?? 0
+ if (left > right) return 1
+ if (left < right) return -1
+ }
+ return 0
+}
diff --git a/packages/desktop-electron/src/main/constants.ts b/packages/desktop-electron/src/main/constants.ts
new file mode 100644
index 000000000..1e21661c1
--- /dev/null
+++ b/packages/desktop-electron/src/main/constants.ts
@@ -0,0 +1,10 @@
+import { app } from "electron"
+
+type Channel = "dev" | "beta" | "prod"
+const raw = import.meta.env.OPENCODE_CHANNEL
+export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod" ? raw : "dev"
+
+export const SETTINGS_STORE = "opencode.settings"
+export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
+export const WSL_ENABLED_KEY = "wslEnabled"
+export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev"
diff --git a/packages/desktop-electron/src/main/env.d.ts b/packages/desktop-electron/src/main/env.d.ts
new file mode 100644
index 000000000..0ee0c551d
--- /dev/null
+++ b/packages/desktop-electron/src/main/env.d.ts
@@ -0,0 +1,7 @@
+interface ImportMetaEnv {
+ readonly OPENCODE_CHANNEL: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts
new file mode 100644
index 000000000..03c1e128e
--- /dev/null
+++ b/packages/desktop-electron/src/main/index.ts
@@ -0,0 +1,449 @@
+import { app, BrowserWindow, dialog } from "electron"
+import type { Event } from "electron"
+import pkg from "electron-updater"
+import { randomUUID } from "node:crypto"
+import { EventEmitter } from "node:events"
+import { existsSync } from "node:fs"
+import { homedir } from "node:os"
+import { join } from "node:path"
+import { createServer } from "node:net"
+
+const APP_NAMES: Record<string, string> = { dev: "OpenCode Dev", beta: "OpenCode Beta", prod: "OpenCode" }
+const APP_IDS: Record<string, string> = {
+ dev: "ai.opencode.desktop.dev",
+ beta: "ai.opencode.desktop.beta",
+ prod: "ai.opencode.desktop",
+}
+app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev")
+app.setPath("userData", join(app.getPath("appData"), app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"))
+const { autoUpdater } = pkg
+
+import { checkAppExists, resolveAppPath, wslPath } from "./apps"
+import { installCli, syncCli } from "./cli"
+import { CHANNEL, UPDATER_ENABLED } from "./constants"
+import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
+import { initLogging } from "./logging"
+import { parseMarkdown } from "./markdown"
+import { createMenu } from "./menu"
+import {
+ checkHealth,
+ checkHealthOrAskRetry,
+ getDefaultServerUrl,
+ getSavedServerUrl,
+ getWslConfig,
+ setDefaultServerUrl,
+ setWslConfig,
+ spawnLocalServer,
+} from "./server"
+import { createLoadingWindow, createMainWindow, setDockIcon } from "./windows"
+
+import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
+import type { CommandChild } from "./cli"
+
+type ServerConnection =
+ | { variant: "existing"; url: string }
+ | {
+ variant: "cli"
+ url: string
+ password: null | string
+ health: {
+ wait: Promise<void>
+ }
+ events: any
+ }
+
+const initEmitter = new EventEmitter()
+let initStep: InitStep = { phase: "server_waiting" }
+
+let mainWindow: BrowserWindow | null = null
+let loadingWindow: BrowserWindow | null = null
+let sidecar: CommandChild | null = null
+let loadingComplete = defer<void>()
+
+const pendingDeepLinks: string[] = []
+
+const serverReady = defer<ServerReadyData>()
+const logger = initLogging()
+
+logger.log("app starting", { version: app.getVersion(), packaged: app.isPackaged })
+
+setupApp()
+
+function setupApp() {
+ ensureLoopbackNoProxy()
+ app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
+
+ if (!app.requestSingleInstanceLock()) {
+ app.quit()
+ return
+ }
+
+ app.on("second-instance", (_event: Event, argv: string[]) => {
+ const urls = argv.filter((arg: string) => arg.startsWith("opencode://"))
+ if (urls.length) {
+ logger.log("deep link received via second-instance", { urls })
+ emitDeepLinks(urls)
+ }
+ focusMainWindow()
+ })
+
+ app.on("open-url", (event: Event, url: string) => {
+ event.preventDefault()
+ logger.log("deep link received via open-url", { url })
+ emitDeepLinks([url])
+ })
+
+ app.on("before-quit", () => {
+ killSidecar()
+ })
+
+ void app.whenReady().then(async () => {
+ // migrate()
+ app.setAsDefaultProtocolClient("opencode")
+ setDockIcon()
+ setupAutoUpdater()
+ syncCli()
+ await initialize()
+ })
+}
+
+function emitDeepLinks(urls: string[]) {
+ if (urls.length === 0) return
+ pendingDeepLinks.push(...urls)
+ if (mainWindow) sendDeepLinks(mainWindow, urls)
+}
+
+function focusMainWindow() {
+ if (!mainWindow) return
+ mainWindow.show()
+ mainWindow.focus()
+}
+
+function setInitStep(step: InitStep) {
+ initStep = step
+ logger.log("init step", { step })
+ initEmitter.emit("step", step)
+}
+
+async function setupServerConnection(): Promise<ServerConnection> {
+ const customUrl = await getSavedServerUrl()
+
+ if (customUrl && (await checkHealthOrAskRetry(customUrl))) {
+ serverReady.resolve({ url: customUrl, password: null })
+ return { variant: "existing", url: customUrl }
+ }
+
+ const port = await getSidecarPort()
+ const hostname = "127.0.0.1"
+ const localUrl = `http://${hostname}:${port}`
+
+ if (await checkHealth(localUrl)) {
+ serverReady.resolve({ url: localUrl, password: null })
+ return { variant: "existing", url: localUrl }
+ }
+
+ const password = randomUUID()
+ const { child, health, events } = spawnLocalServer(hostname, port, password)
+ sidecar = child
+
+ return {
+ variant: "cli",
+ url: localUrl,
+ password,
+ health,
+ events,
+ }
+}
+
+async function initialize() {
+ const needsMigration = !sqliteFileExists()
+ const sqliteDone = needsMigration ? defer<void>() : undefined
+
+ const loadingTask = (async () => {
+ logger.log("setting up server connection")
+ const serverConnection = await setupServerConnection()
+ logger.log("server connection ready", { variant: serverConnection.variant, url: serverConnection.url })
+
+ const cliHealthCheck = (() => {
+ if (serverConnection.variant == "cli") {
+ return async () => {
+ const { events, health } = serverConnection
+ events.on("sqlite", (progress: SqliteMigrationProgress) => {
+ setInitStep({ phase: "sqlite_waiting" })
+ if (loadingWindow) sendSqliteMigrationProgress(loadingWindow, progress)
+ if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
+ if (progress.type === "Done") sqliteDone?.resolve()
+ })
+ await health.wait
+ serverReady.resolve({ url: serverConnection.url, password: serverConnection.password })
+ }
+ } else {
+ serverReady.resolve({ url: serverConnection.url, password: null })
+ return null
+ }
+ })()
+
+ logger.log("server connection started")
+
+ if (cliHealthCheck) {
+ if (needsMigration) await sqliteDone?.promise
+ cliHealthCheck?.()
+ }
+
+ logger.log("loading task finished")
+ })()
+
+ const globals = {
+ updaterEnabled: UPDATER_ENABLED,
+ wsl: getWslConfig().enabled,
+ deepLinks: pendingDeepLinks,
+ }
+
+ const loadingWindow = await (async () => {
+ if (needsMigration /** TOOD: 1 second timeout */) {
+ // showLoading = await Promise.race([init.then(() => false).catch(() => false), delay(1000).then(() => true)])
+ const loadingWindow = createLoadingWindow(globals)
+ await delay(1000)
+ return loadingWindow
+ } else {
+ logger.log("showing main window without loading window")
+ mainWindow = createMainWindow(globals)
+ wireMenu()
+ }
+ })()
+
+ await loadingTask
+ setInitStep({ phase: "done" })
+
+ if (loadingWindow) {
+ await loadingComplete.promise
+ }
+
+ if (!mainWindow) {
+ mainWindow = createMainWindow(globals)
+ wireMenu()
+ }
+
+ loadingWindow?.close()
+}
+
+function wireMenu() {
+ if (!mainWindow) return
+ createMenu({
+ trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id),
+ installCli: () => {
+ void installCli()
+ },
+ checkForUpdates: () => {
+ void checkForUpdates(true)
+ },
+ reload: () => mainWindow?.reload(),
+ relaunch: () => {
+ killSidecar()
+ app.relaunch()
+ app.exit(0)
+ },
+ })
+}
+
+registerIpcHandlers({
+ killSidecar: () => killSidecar(),
+ installCli: async () => installCli(),
+ awaitInitialization: async (sendStep) => {
+ sendStep(initStep)
+ const listener = (step: InitStep) => sendStep(step)
+ initEmitter.on("step", listener)
+ try {
+ logger.log("awaiting server ready")
+ const res = await serverReady.promise
+ logger.log("server ready", { url: res.url })
+ return res
+ } finally {
+ initEmitter.off("step", listener)
+ }
+ },
+ getDefaultServerUrl: () => getDefaultServerUrl(),
+ setDefaultServerUrl: (url) => setDefaultServerUrl(url),
+ getWslConfig: () => Promise.resolve(getWslConfig()),
+ setWslConfig: (config: WslConfig) => setWslConfig(config),
+ getDisplayBackend: async () => null,
+ setDisplayBackend: async () => undefined,
+ parseMarkdown: async (markdown) => parseMarkdown(markdown),
+ checkAppExists: async (appName) => checkAppExists(appName),
+ wslPath: async (path, mode) => wslPath(path, mode),
+ resolveAppPath: async (appName) => resolveAppPath(appName),
+ loadingWindowComplete: () => loadingComplete.resolve(),
+ runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail),
+ checkUpdate: async () => checkUpdate(),
+ installUpdate: async () => installUpdate(),
+})
+
+function killSidecar() {
+ if (!sidecar) return
+ sidecar.kill()
+ sidecar = null
+}
+
+function ensureLoopbackNoProxy() {
+ const loopback = ["127.0.0.1", "localhost", "::1"]
+ const upsert = (key: string) => {
+ const items = (process.env[key] ?? "")
+ .split(",")
+ .map((value: string) => value.trim())
+ .filter((value: string) => Boolean(value))
+
+ for (const host of loopback) {
+ if (items.some((value: string) => value.toLowerCase() === host)) continue
+ items.push(host)
+ }
+
+ process.env[key] = items.join(",")
+ }
+
+ upsert("NO_PROXY")
+ upsert("no_proxy")
+}
+
+async function getSidecarPort() {
+ const fromEnv = process.env.OPENCODE_PORT
+ if (fromEnv) {
+ const parsed = Number.parseInt(fromEnv, 10)
+ if (!Number.isNaN(parsed)) return parsed
+ }
+
+ return await new Promise<number>((resolve, reject) => {
+ const server = createServer()
+ server.on("error", reject)
+ server.listen(0, "127.0.0.1", () => {
+ const address = server.address()
+ if (typeof address !== "object" || !address) {
+ server.close()
+ reject(new Error("Failed to get port"))
+ return
+ }
+ const port = address.port
+ server.close(() => resolve(port))
+ })
+ })
+}
+
+function sqliteFileExists() {
+ const xdg = process.env.XDG_DATA_HOME
+ const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".local", "share")
+ return existsSync(join(base, "opencode", "opencode.db"))
+}
+
+function setupAutoUpdater() {
+ if (!UPDATER_ENABLED) return
+ autoUpdater.logger = logger
+ autoUpdater.channel = "latest"
+ autoUpdater.allowPrerelease = false
+ autoUpdater.allowDowngrade = true
+ autoUpdater.autoDownload = false
+ autoUpdater.autoInstallOnAppQuit = true
+ logger.log("auto updater configured", {
+ channel: autoUpdater.channel,
+ allowPrerelease: autoUpdater.allowPrerelease,
+ allowDowngrade: autoUpdater.allowDowngrade,
+ currentVersion: app.getVersion(),
+ })
+}
+
+let updateReady = false
+
+async function checkUpdate() {
+ if (!UPDATER_ENABLED) return { updateAvailable: false }
+ updateReady = false
+ logger.log("checking for updates", {
+ currentVersion: app.getVersion(),
+ channel: autoUpdater.channel,
+ allowPrerelease: autoUpdater.allowPrerelease,
+ allowDowngrade: autoUpdater.allowDowngrade,
+ })
+ try {
+ const result = await autoUpdater.checkForUpdates()
+ const updateInfo = result?.updateInfo
+ logger.log("update metadata fetched", {
+ releaseVersion: updateInfo?.version ?? null,
+ releaseDate: updateInfo?.releaseDate ?? null,
+ releaseName: updateInfo?.releaseName ?? null,
+ files: updateInfo?.files?.map((file) => file.url) ?? [],
+ })
+ const version = result?.updateInfo?.version
+ if (!version) {
+ logger.log("no update available", { reason: "provider returned no newer version" })
+ return { updateAvailable: false }
+ }
+ logger.log("update available", { version })
+ await autoUpdater.downloadUpdate()
+ logger.log("update download completed", { version })
+ updateReady = true
+ return { updateAvailable: true, version }
+ } catch (error) {
+ logger.error("update check failed", error)
+ return { updateAvailable: false, failed: true }
+ }
+}
+
+async function installUpdate() {
+ if (!updateReady) return
+ killSidecar()
+ autoUpdater.quitAndInstall()
+}
+
+async function checkForUpdates(alertOnFail: boolean) {
+ if (!UPDATER_ENABLED) return
+ logger.log("checkForUpdates invoked", { alertOnFail })
+ const result = await checkUpdate()
+ if (!result.updateAvailable) {
+ if (result.failed) {
+ logger.log("no update decision", { reason: "update check failed" })
+ if (!alertOnFail) return
+ await dialog.showMessageBox({
+ type: "error",
+ message: "Update check failed.",
+ title: "Update Error",
+ })
+ return
+ }
+
+ logger.log("no update decision", { reason: "already up to date" })
+ if (!alertOnFail) return
+ await dialog.showMessageBox({
+ type: "info",
+ message: "You're up to date.",
+ title: "No Updates",
+ })
+ return
+ }
+
+ const response = await dialog.showMessageBox({
+ type: "info",
+ message: `Update ${result.version ?? ""} downloaded. Restart now?`,
+ title: "Update Ready",
+ buttons: ["Restart", "Later"],
+ defaultId: 0,
+ cancelId: 1,
+ })
+ logger.log("update prompt response", {
+ version: result.version ?? null,
+ restartNow: response.response === 0,
+ })
+ if (response.response === 0) {
+ await installUpdate()
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+function defer<T>() {
+ let resolve!: (value: T) => void
+ let reject!: (error: Error) => void
+ const promise = new Promise<T>((res, rej) => {
+ resolve = res
+ reject = rej
+ })
+ return { promise, resolve, reject }
+}
diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts
new file mode 100644
index 000000000..bbb5379bb
--- /dev/null
+++ b/packages/desktop-electron/src/main/ipc.ts
@@ -0,0 +1,176 @@
+import { execFile } from "node:child_process"
+import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
+import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
+
+import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
+import { getStore } from "./store"
+
+type Deps = {
+ killSidecar: () => void
+ installCli: () => Promise<string>
+ awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
+ getDefaultServerUrl: () => Promise<string | null> | string | null
+ setDefaultServerUrl: (url: string | null) => Promise<void> | void
+ getWslConfig: () => Promise<WslConfig>
+ setWslConfig: (config: WslConfig) => Promise<void> | void
+ getDisplayBackend: () => Promise<string | null>
+ setDisplayBackend: (backend: string | null) => Promise<void> | void
+ parseMarkdown: (markdown: string) => Promise<string> | string
+ checkAppExists: (appName: string) => Promise<boolean> | boolean
+ wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
+ resolveAppPath: (appName: string) => Promise<string | null>
+ loadingWindowComplete: () => void
+ runUpdater: (alertOnFail: boolean) => Promise<void> | void
+ checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
+ installUpdate: () => Promise<void> | void
+}
+
+export function registerIpcHandlers(deps: Deps) {
+ ipcMain.handle("kill-sidecar", () => deps.killSidecar())
+ ipcMain.handle("install-cli", () => deps.installCli())
+ ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
+ const send = (step: InitStep) => event.sender.send("init-step", step)
+ return deps.awaitInitialization(send)
+ })
+ ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
+ ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
+ deps.setDefaultServerUrl(url),
+ )
+ ipcMain.handle("get-wsl-config", () => deps.getWslConfig())
+ ipcMain.handle("set-wsl-config", (_event: IpcMainInvokeEvent, config: WslConfig) => deps.setWslConfig(config))
+ ipcMain.handle("get-display-backend", () => deps.getDisplayBackend())
+ ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
+ deps.setDisplayBackend(backend),
+ )
+ ipcMain.handle("parse-markdown", (_event: IpcMainInvokeEvent, markdown: string) => deps.parseMarkdown(markdown))
+ ipcMain.handle("check-app-exists", (_event: IpcMainInvokeEvent, appName: string) => deps.checkAppExists(appName))
+ ipcMain.handle("wsl-path", (_event: IpcMainInvokeEvent, path: string, mode: "windows" | "linux" | null) =>
+ deps.wslPath(path, mode),
+ )
+ ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
+ ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete())
+ ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
+ ipcMain.handle("check-update", () => deps.checkUpdate())
+ ipcMain.handle("install-update", () => deps.installUpdate())
+ ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
+ const store = getStore(name)
+ const value = store.get(key)
+ if (value === undefined || value === null) return null
+ return typeof value === "string" ? value : JSON.stringify(value)
+ })
+ ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
+ getStore(name).set(key, value)
+ })
+ ipcMain.handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => {
+ getStore(name).delete(key)
+ })
+ ipcMain.handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => {
+ getStore(name).clear()
+ })
+ ipcMain.handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => {
+ const store = getStore(name)
+ return Object.keys(store.store)
+ })
+ ipcMain.handle("store-length", (_event: IpcMainInvokeEvent, name: string) => {
+ const store = getStore(name)
+ return Object.keys(store.store).length
+ })
+
+ ipcMain.handle(
+ "open-directory-picker",
+ async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
+ const result = await dialog.showOpenDialog({
+ properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : [])],
+ title: opts?.title ?? "Choose a folder",
+ defaultPath: opts?.defaultPath,
+ })
+ if (result.canceled) return null
+ return opts?.multiple ? result.filePaths : result.filePaths[0]
+ },
+ )
+
+ ipcMain.handle(
+ "open-file-picker",
+ async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
+ const result = await dialog.showOpenDialog({
+ properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])],
+ title: opts?.title ?? "Choose a file",
+ defaultPath: opts?.defaultPath,
+ })
+ if (result.canceled) return null
+ return opts?.multiple ? result.filePaths : result.filePaths[0]
+ },
+ )
+
+ ipcMain.handle(
+ "save-file-picker",
+ async (_event: IpcMainInvokeEvent, opts?: { title?: string; defaultPath?: string }) => {
+ const result = await dialog.showSaveDialog({
+ title: opts?.title ?? "Save file",
+ defaultPath: opts?.defaultPath,
+ })
+ if (result.canceled) return null
+ return result.filePath ?? null
+ },
+ )
+
+ ipcMain.on("open-link", (_event: IpcMainEvent, url: string) => {
+ void shell.openExternal(url)
+ })
+
+ ipcMain.handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => {
+ if (!app) return shell.openPath(path)
+ await new Promise<void>((resolve, reject) => {
+ const [cmd, args] =
+ process.platform === "darwin" ? (["open", ["-a", app, path]] as const) : ([app, [path]] as const)
+ execFile(cmd, args, (err) => (err ? reject(err) : resolve()))
+ })
+ })
+
+ ipcMain.handle("read-clipboard-image", () => {
+ const image = clipboard.readImage()
+ if (image.isEmpty()) return null
+ const buffer = image.toPNG().buffer
+ const size = image.getSize()
+ return { buffer, width: size.width, height: size.height }
+ })
+
+ ipcMain.on("show-notification", (_event: IpcMainEvent, title: string, body?: string) => {
+ new Notification({ title, body }).show()
+ })
+
+ ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => {
+ const win = BrowserWindow.fromWebContents(event.sender)
+ return win?.isFocused() ?? false
+ })
+
+ ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => {
+ const win = BrowserWindow.fromWebContents(event.sender)
+ win?.focus()
+ })
+
+ ipcMain.handle("show-window", (event: IpcMainInvokeEvent) => {
+ const win = BrowserWindow.fromWebContents(event.sender)
+ win?.show()
+ })
+
+ ipcMain.on("relaunch", () => {
+ app.relaunch()
+ app.exit(0)
+ })
+
+ ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
+ ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor))
+}
+
+export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) {
+ win.webContents.send("sqlite-migration-progress", progress)
+}
+
+export function sendMenuCommand(win: BrowserWindow, id: string) {
+ win.webContents.send("menu-command", id)
+}
+
+export function sendDeepLinks(win: BrowserWindow, urls: string[]) {
+ win.webContents.send("deep-link", urls)
+}
diff --git a/packages/desktop-electron/src/main/logging.ts b/packages/desktop-electron/src/main/logging.ts
new file mode 100644
index 000000000..d315b2d34
--- /dev/null
+++ b/packages/desktop-electron/src/main/logging.ts
@@ -0,0 +1,40 @@
+import log from "electron-log/main.js"
+import { readFileSync, readdirSync, statSync, unlinkSync } from "node:fs"
+import { dirname, join } from "node:path"
+
+const MAX_LOG_AGE_DAYS = 7
+const TAIL_LINES = 1000
+
+export function initLogging() {
+ log.transports.file.maxSize = 5 * 1024 * 1024
+ cleanup()
+ return log
+}
+
+export function tail(): string {
+ try {
+ const path = log.transports.file.getFile().path
+ const contents = readFileSync(path, "utf8")
+ const lines = contents.split("\n")
+ return lines.slice(Math.max(0, lines.length - TAIL_LINES)).join("\n")
+ } catch {
+ return ""
+ }
+}
+
+function cleanup() {
+ const path = log.transports.file.getFile().path
+ const dir = dirname(path)
+ const cutoff = Date.now() - MAX_LOG_AGE_DAYS * 24 * 60 * 60 * 1000
+
+ for (const entry of readdirSync(dir)) {
+ const file = join(dir, entry)
+ try {
+ const info = statSync(file)
+ if (!info.isFile()) continue
+ if (info.mtimeMs < cutoff) unlinkSync(file)
+ } catch {
+ continue
+ }
+ }
+}
diff --git a/packages/desktop-electron/src/main/markdown.ts b/packages/desktop-electron/src/main/markdown.ts
new file mode 100644
index 000000000..b956f4876
--- /dev/null
+++ b/packages/desktop-electron/src/main/markdown.ts
@@ -0,0 +1,16 @@
+import { marked, type Tokens } from "marked"
+
+const renderer = new marked.Renderer()
+
+renderer.link = ({ href, title, text }: Tokens.Link) => {
+ const titleAttr = title ? ` title="${title}"` : ""
+ return `<a href="${href}"${titleAttr} class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>`
+}
+
+export function parseMarkdown(input: string) {
+ return marked(input, {
+ renderer,
+ breaks: false,
+ gfm: true,
+ })
+}
diff --git a/packages/desktop-electron/src/main/menu.ts b/packages/desktop-electron/src/main/menu.ts
new file mode 100644
index 000000000..53707ba7f
--- /dev/null
+++ b/packages/desktop-electron/src/main/menu.ts
@@ -0,0 +1,116 @@
+import { BrowserWindow, Menu, shell } from "electron"
+
+import { UPDATER_ENABLED } from "./constants"
+
+type Deps = {
+ trigger: (id: string) => void
+ installCli: () => void
+ checkForUpdates: () => void
+ reload: () => void
+ relaunch: () => void
+}
+
+export function createMenu(deps: Deps) {
+ if (process.platform !== "darwin") return
+
+ const template: Electron.MenuItemConstructorOptions[] = [
+ {
+ label: "OpenCode",
+ submenu: [
+ { role: "about" },
+ {
+ label: "Check for Updates...",
+ enabled: UPDATER_ENABLED,
+ click: () => deps.checkForUpdates(),
+ },
+ {
+ label: "Install CLI...",
+ click: () => deps.installCli(),
+ },
+ {
+ label: "Reload Webview",
+ click: () => deps.reload(),
+ },
+ {
+ label: "Restart",
+ click: () => deps.relaunch(),
+ },
+ { type: "separator" },
+ { role: "hide" },
+ { role: "hideOthers" },
+ { role: "unhide" },
+ { type: "separator" },
+ { role: "quit" },
+ ],
+ },
+ {
+ label: "File",
+ submenu: [
+ { label: "New Session", accelerator: "Shift+Cmd+S", click: () => deps.trigger("session.new") },
+ { label: "Open Project...", accelerator: "Cmd+O", click: () => deps.trigger("project.open") },
+ { type: "separator" },
+ { role: "close" },
+ ],
+ },
+ {
+ label: "Edit",
+ submenu: [
+ { role: "undo" },
+ { role: "redo" },
+ { type: "separator" },
+ { role: "cut" },
+ { role: "copy" },
+ { role: "paste" },
+ { role: "selectAll" },
+ ],
+ },
+ {
+ label: "View",
+ submenu: [
+ { label: "Toggle Sidebar", accelerator: "Cmd+B", click: () => deps.trigger("sidebar.toggle") },
+ { label: "Toggle Terminal", accelerator: "Ctrl+`", click: () => deps.trigger("terminal.toggle") },
+ { label: "Toggle File Tree", click: () => deps.trigger("fileTree.toggle") },
+ { type: "separator" },
+ { label: "Back", click: () => deps.trigger("common.goBack") },
+ { label: "Forward", click: () => deps.trigger("common.goForward") },
+ { type: "separator" },
+ {
+ label: "Previous Session",
+ accelerator: "Option+ArrowUp",
+ click: () => deps.trigger("session.previous"),
+ },
+ {
+ label: "Next Session",
+ accelerator: "Option+ArrowDown",
+ click: () => deps.trigger("session.next"),
+ },
+ { type: "separator" },
+ {
+ label: "Toggle Developer Tools",
+ accelerator: "Alt+Cmd+I",
+ click: () => BrowserWindow.getFocusedWindow()?.webContents.toggleDevTools(),
+ },
+ ],
+ },
+ {
+ label: "Help",
+ submenu: [
+ { label: "OpenCode Documentation", click: () => shell.openExternal("https://opencode.ai/docs") },
+ { label: "Support Forum", click: () => shell.openExternal("https://discord.com/invite/opencode") },
+ { type: "separator" },
+ { type: "separator" },
+ {
+ label: "Share Feedback",
+ click: () =>
+ shell.openExternal("https://github.com/anomalyco/opencode/issues/new?template=feature_request.yml"),
+ },
+ {
+ label: "Report a Bug",
+ click: () => shell.openExternal("https://github.com/anomalyco/opencode/issues/new?template=bug_report.yml"),
+ },
+ ],
+ },
+ ]
+
+ Menu.setApplicationMenu(Menu.buildFromTemplate(template))
+}
diff --git a/packages/desktop-electron/src/main/migrate.ts b/packages/desktop-electron/src/main/migrate.ts
new file mode 100644
index 000000000..bad1349ee
--- /dev/null
+++ b/packages/desktop-electron/src/main/migrate.ts
@@ -0,0 +1,91 @@
+import { app } from "electron"
+import log from "electron-log/main.js"
+import { existsSync, readdirSync, readFileSync } from "node:fs"
+import { homedir } from "node:os"
+import { join } from "node:path"
+import { CHANNEL } from "./constants"
+import { getStore, store } from "./store"
+
+const TAURI_MIGRATED_KEY = "tauriMigrated"
+
+// Resolve the directory where Tauri stored its .dat files for the given app identifier.
+// Mirrors Tauri's AppLocalData / AppData resolution per OS.
+function tauriDir(id: string) {
+ switch (process.platform) {
+ case "darwin":
+ return join(homedir(), "Library", "Application Support", id)
+ case "win32":
+ return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), id)
+ default:
+ return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), id)
+ }
+}
+
+// The Tauri app identifier changes between dev/beta/prod builds.
+const TAURI_APP_IDS: Record<string, string> = {
+ dev: "ai.opencode.desktop.dev",
+ beta: "ai.opencode.desktop.beta",
+ prod: "ai.opencode.desktop",
+}
+function tauriAppId() {
+ return app.isPackaged ? TAURI_APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"
+}
+
+// Migrate a single Tauri .dat file into the corresponding electron-store.
+// `opencode.settings.dat` is special: it maps to the `opencode.settings` store
+// (the electron-store name without the `.dat` extension). All other .dat files
+// keep their full filename as the electron-store name so they match what the
+// renderer already passes via IPC (e.g. `"default.dat"`, `"opencode.global.dat"`).
+function migrateFile(datPath: string, filename: string) {
+ let data: Record<string, unknown>
+ try {
+ data = JSON.parse(readFileSync(datPath, "utf-8"))
+ } catch (err) {
+ log.warn("tauri migration: failed to parse", filename, err)
+ return
+ }
+
+ // opencode.settings.dat → the electron settings store ("opencode.settings").
+ // All other .dat files keep their full filename as the store name so they match
+ // what the renderer passes via IPC (e.g. "default.dat", "opencode.global.dat").
+ const storeName = filename === "opencode.settings.dat" ? "opencode.settings" : filename
+ const target = getStore(storeName)
+ const migrated: string[] = []
+ const skipped: string[] = []
+
+ for (const [key, value] of Object.entries(data)) {
+ // Don't overwrite values the user has already set in the Electron app.
+ if (target.has(key)) {
+ skipped.push(key)
+ continue
+ }
+ target.set(key, value)
+ migrated.push(key)
+ }
+
+ log.log("tauri migration: migrated", filename, "→", storeName, { migrated, skipped })
+}
+
+export function migrate() {
+ if (store.get(TAURI_MIGRATED_KEY)) {
+ log.log("tauri migration: already done, skipping")
+ return
+ }
+
+ const dir = tauriDir(tauriAppId())
+ log.log("tauri migration: starting", { dir })
+
+ if (!existsSync(dir)) {
+ log.log("tauri migration: no tauri data directory found, nothing to migrate")
+ store.set(TAURI_MIGRATED_KEY, true)
+ return
+ }
+
+ for (const filename of readdirSync(dir)) {
+ if (!filename.endsWith(".dat")) continue
+ migrateFile(join(dir, filename), filename)
+ }
+
+ log.log("tauri migration: complete")
+ store.set(TAURI_MIGRATED_KEY, true)
+}
diff --git a/packages/desktop-electron/src/main/server.ts b/packages/desktop-electron/src/main/server.ts
new file mode 100644
index 000000000..92018e72e
--- /dev/null
+++ b/packages/desktop-electron/src/main/server.ts
@@ -0,0 +1,129 @@
+import { dialog } from "electron"
+
+import { getConfig, serve, type CommandChild, type Config } from "./cli"
+import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
+import { store } from "./store"
+
+export type WslConfig = { enabled: boolean }
+
+export type HealthCheck = { wait: Promise<void> }
+
+export function getDefaultServerUrl(): string | null {
+ const value = store.get(DEFAULT_SERVER_URL_KEY)
+ return typeof value === "string" ? value : null
+}
+
+export function setDefaultServerUrl(url: string | null) {
+ if (url) {
+ store.set(DEFAULT_SERVER_URL_KEY, url)
+ return
+ }
+
+ store.delete(DEFAULT_SERVER_URL_KEY)
+}
+
+export function getWslConfig(): WslConfig {
+ const value = store.get(WSL_ENABLED_KEY)
+ return { enabled: typeof value === "boolean" ? value : false }
+}
+
+export function setWslConfig(config: WslConfig) {
+ store.set(WSL_ENABLED_KEY, config.enabled)
+}
+
+export async function getSavedServerUrl(): Promise<string | null> {
+ const direct = getDefaultServerUrl()
+ if (direct) return direct
+
+ const config = await getConfig().catch(() => null)
+ if (!config) return null
+ return getServerUrlFromConfig(config)
+}
+
+export function spawnLocalServer(hostname: string, port: number, password: string) {
+ const { child, exit, events } = serve(hostname, port, password)
+
+ const wait = (async () => {
+ const url = `http://${hostname}:${port}`
+
+ const ready = async () => {
+ while (true) {
+ await new Promise((resolve) => setTimeout(resolve, 100))
+ if (await checkHealth(url, password)) return
+ }
+ }
+
+ const terminated = async () => {
+ const payload = await exit
+ throw new Error(
+ `Sidecar terminated before becoming healthy (code=${payload.code ?? "unknown"} signal=${
+ payload.signal ?? "unknown"
+ })`,
+ )
+ }
+
+ await Promise.race([ready(), terminated()])
+ })()
+
+ return { child, health: { wait }, events }
+}
+
+export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
+ let healthUrl: URL
+ try {
+ healthUrl = new URL("/global/health", url)
+ } catch {
+ return false
+ }
+
+ const headers = new Headers()
+ if (password) {
+ const auth = Buffer.from(`opencode:${password}`).toString("base64")
+ headers.set("authorization", `Basic ${auth}`)
+ }
+
+ try {
+ const res = await fetch(healthUrl, {
+ method: "GET",
+ headers,
+ signal: AbortSignal.timeout(3000),
+ })
+ return res.ok
+ } catch {
+ return false
+ }
+}
+
+export async function checkHealthOrAskRetry(url: string): Promise<boolean> {
+ while (true) {
+ if (await checkHealth(url)) return true
+
+ const result = await dialog.showMessageBox({
+ type: "warning",
+ message: `Could not connect to configured server:\n${url}\n\nWould you like to retry or start a local server instead?`,
+ title: "Connection Failed",
+ buttons: ["Retry", "Start Local"],
+ defaultId: 0,
+ cancelId: 1,
+ })
+
+ if (result.response === 0) continue
+ return false
+ }
+}
+
+export function normalizeHostnameForUrl(hostname: string) {
+ if (hostname === "0.0.0.0") return "127.0.0.1"
+ if (hostname === "::") return "[::1]"
+ if (hostname.includes(":") && !hostname.startsWith("[")) return `[${hostname}]`
+ return hostname
+}
+
+export function getServerUrlFromConfig(config: Config) {
+ const server = config.server
+ if (!server?.port) return null
+ const host = server.hostname ? normalizeHostnameForUrl(server.hostname) : "127.0.0.1"
+ return `http://${host}:${server.port}`
+}
+
+export type { CommandChild }
diff --git a/packages/desktop-electron/src/main/store.ts b/packages/desktop-electron/src/main/store.ts
new file mode 100644
index 000000000..fa1c5682e
--- /dev/null
+++ b/packages/desktop-electron/src/main/store.ts
@@ -0,0 +1,15 @@
+import Store from "electron-store"
+
+import { SETTINGS_STORE } from "./constants"
+
+const cache = new Map<string, Store>()
+
+export function getStore(name = SETTINGS_STORE) {
+ const cached = cache.get(name)
+ if (cached) return cached
+ const next = new Store({ name })
+ cache.set(name, next)
+ return next
+}
+
+export const store = getStore(SETTINGS_STORE)
diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts
new file mode 100644
index 000000000..9178457f8
--- /dev/null
+++ b/packages/desktop-electron/src/main/windows.ts
@@ -0,0 +1,135 @@
+import windowState from "electron-window-state"
+import { app, BrowserWindow, nativeImage } from "electron"
+import { dirname, join } from "node:path"
+import { fileURLToPath } from "node:url"
+
+type Globals = {
+ updaterEnabled: boolean
+ wsl: boolean
+ deepLinks?: string[]
+}
+
+const root = dirname(fileURLToPath(import.meta.url))
+
+function iconsDir() {
+ return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
+}
+
+function iconPath() {
+ const ext = process.platform === "win32" ? "ico" : "png"
+ return join(iconsDir(), `icon.${ext}`)
+}
+
+export function setDockIcon() {
+ if (process.platform !== "darwin") return
+ app.dock?.setIcon(nativeImage.createFromPath(join(iconsDir(), "[email protected]")))
+}
+
+export function createMainWindow(globals: Globals) {
+ const state = windowState({
+ defaultWidth: 1280,
+ defaultHeight: 800,
+ })
+
+ const win = new BrowserWindow({
+ x: state.x,
+ y: state.y,
+ width: state.width,
+ height: state.height,
+ show: true,
+ title: "OpenCode",
+ icon: iconPath(),
+ ...(process.platform === "darwin"
+ ? {
+ titleBarStyle: "hidden" as const,
+ trafficLightPosition: { x: 12, y: 14 },
+ }
+ : {}),
+ ...(process.platform === "win32"
+ ? {
+ frame: false,
+ titleBarStyle: "hidden" as const,
+ titleBarOverlay: {
+ color: "transparent",
+ symbolColor: "#999",
+ height: 40,
+ },
+ }
+ : {}),
+ webPreferences: {
+ preload: join(root, "../preload/index.mjs"),
+ sandbox: false,
+ },
+ })
+
+ state.manage(win)
+ loadWindow(win, "index.html")
+ wireZoom(win)
+ injectGlobals(win, globals)
+
+ return win
+}
+
+export function createLoadingWindow(globals: Globals) {
+ const win = new BrowserWindow({
+ width: 640,
+ height: 480,
+ resizable: false,
+ center: true,
+ show: true,
+ icon: iconPath(),
+ ...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
+ ...(process.platform === "win32"
+ ? {
+ frame: false,
+ titleBarStyle: "hidden" as const,
+ titleBarOverlay: {
+ color: "transparent",
+ symbolColor: "#999",
+ height: 40,
+ },
+ }
+ : {}),
+ webPreferences: {
+ preload: join(root, "../preload/index.mjs"),
+ sandbox: false,
+ },
+ })
+
+ loadWindow(win, "loading.html")
+ injectGlobals(win, globals)
+
+ return win
+}
+
+function loadWindow(win: BrowserWindow, html: string) {
+ const devUrl = process.env.ELECTRON_RENDERER_URL
+ if (devUrl) {
+ const url = new URL(html, devUrl)
+ void win.loadURL(url.toString())
+ return
+ }
+
+ void win.loadFile(join(root, `../renderer/${html}`))
+}
+
+function injectGlobals(win: BrowserWindow, globals: Globals) {
+ win.webContents.on("dom-ready", () => {
+ const deepLinks = globals.deepLinks ?? []
+ const data = {
+ updaterEnabled: globals.updaterEnabled,
+ wsl: globals.wsl,
+ deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks,
+ }
+ void win.webContents.executeJavaScript(
+ `window.__OPENCODE__ = Object.assign(window.__OPENCODE__ ?? {}, ${JSON.stringify(data)})`,
+ )
+ })
+}
+
+function wireZoom(win: BrowserWindow) {
+ win.webContents.setZoomFactor(1)
+ win.webContents.on("zoom-changed", () => {
+ win.webContents.setZoomFactor(1)
+ })
+}
diff --git a/packages/desktop-electron/src/preload/index.ts b/packages/desktop-electron/src/preload/index.ts
new file mode 100644
index 000000000..a6520ab42
--- /dev/null
+++ b/packages/desktop-electron/src/preload/index.ts
@@ -0,0 +1,66 @@
+import { contextBridge, ipcRenderer } from "electron"
+import type { ElectronAPI, InitStep, SqliteMigrationProgress } from "./types"
+
+const api: ElectronAPI = {
+ killSidecar: () => ipcRenderer.invoke("kill-sidecar"),
+ installCli: () => ipcRenderer.invoke("install-cli"),
+ awaitInitialization: (onStep) => {
+ const handler = (_: unknown, step: InitStep) => onStep(step)
+ ipcRenderer.on("init-step", handler)
+ return ipcRenderer.invoke("await-initialization").finally(() => {
+ ipcRenderer.removeListener("init-step", handler)
+ })
+ },
+ getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
+ setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
+ getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
+ setWslConfig: (config) => ipcRenderer.invoke("set-wsl-config", config),
+ getDisplayBackend: () => ipcRenderer.invoke("get-display-backend"),
+ setDisplayBackend: (backend) => ipcRenderer.invoke("set-display-backend", backend),
+ parseMarkdownCommand: (markdown) => ipcRenderer.invoke("parse-markdown", markdown),
+ checkAppExists: (appName) => ipcRenderer.invoke("check-app-exists", appName),
+ wslPath: (path, mode) => ipcRenderer.invoke("wsl-path", path, mode),
+ resolveAppPath: (appName) => ipcRenderer.invoke("resolve-app-path", appName),
+ storeGet: (name, key) => ipcRenderer.invoke("store-get", name, key),
+ storeSet: (name, key, value) => ipcRenderer.invoke("store-set", name, key, value),
+ storeDelete: (name, key) => ipcRenderer.invoke("store-delete", name, key),
+ storeClear: (name) => ipcRenderer.invoke("store-clear", name),
+ storeKeys: (name) => ipcRenderer.invoke("store-keys", name),
+ storeLength: (name) => ipcRenderer.invoke("store-length", name),
+
+ onSqliteMigrationProgress: (cb) => {
+ const handler = (_: unknown, progress: SqliteMigrationProgress) => cb(progress)
+ ipcRenderer.on("sqlite-migration-progress", handler)
+ return () => ipcRenderer.removeListener("sqlite-migration-progress", handler)
+ },
+ onMenuCommand: (cb) => {
+ const handler = (_: unknown, id: string) => cb(id)
+ ipcRenderer.on("menu-command", handler)
+ return () => ipcRenderer.removeListener("menu-command", handler)
+ },
+ onDeepLink: (cb) => {
+ const handler = (_: unknown, urls: string[]) => cb(urls)
+ ipcRenderer.on("deep-link", handler)
+ return () => ipcRenderer.removeListener("deep-link", handler)
+ },
+
+ openDirectoryPicker: (opts) => ipcRenderer.invoke("open-directory-picker", opts),
+ openFilePicker: (opts) => ipcRenderer.invoke("open-file-picker", opts),
+ saveFilePicker: (opts) => ipcRenderer.invoke("save-file-picker", opts),
+ openLink: (url) => ipcRenderer.send("open-link", url),
+ openPath: (path, app) => ipcRenderer.invoke("open-path", path, app),
+ readClipboardImage: () => ipcRenderer.invoke("read-clipboard-image"),
+ showNotification: (title, body) => ipcRenderer.send("show-notification", title, body),
+ getWindowFocused: () => ipcRenderer.invoke("get-window-focused"),
+ setWindowFocus: () => ipcRenderer.invoke("set-window-focus"),
+ showWindow: () => ipcRenderer.invoke("show-window"),
+ relaunch: () => ipcRenderer.send("relaunch"),
+ getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"),
+ setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
+ loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
+ runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
+ checkUpdate: () => ipcRenderer.invoke("check-update"),
+ installUpdate: () => ipcRenderer.invoke("install-update"),
+}
+
+contextBridge.exposeInMainWorld("api", api)
diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts
new file mode 100644
index 000000000..af5410f5f
--- /dev/null
+++ b/packages/desktop-electron/src/preload/types.ts
@@ -0,0 +1,64 @@
+export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" }
+
+export type ServerReadyData = {
+ url: string
+ password: string | null
+}
+
+export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
+
+export type WslConfig = { enabled: boolean }
+
+export type LinuxDisplayBackend = "wayland" | "auto"
+
+export type ElectronAPI = {
+ killSidecar: () => Promise<void>
+ installCli: () => Promise<string>
+ awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
+ getDefaultServerUrl: () => Promise<string | null>
+ setDefaultServerUrl: (url: string | null) => Promise<void>
+ getWslConfig: () => Promise<WslConfig>
+ setWslConfig: (config: WslConfig) => Promise<void>
+ getDisplayBackend: () => Promise<LinuxDisplayBackend | null>
+ setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise<void>
+ parseMarkdownCommand: (markdown: string) => Promise<string>
+ checkAppExists: (appName: string) => Promise<boolean>
+ wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
+ resolveAppPath: (appName: string) => Promise<string | null>
+ storeGet: (name: string, key: string) => Promise<string | null>
+ storeSet: (name: string, key: string, value: string) => Promise<void>
+ storeDelete: (name: string, key: string) => Promise<void>
+ storeClear: (name: string) => Promise<void>
+ storeKeys: (name: string) => Promise<string[]>
+ storeLength: (name: string) => Promise<number>
+
+ onSqliteMigrationProgress: (cb: (progress: SqliteMigrationProgress) => void) => () => void
+ onMenuCommand: (cb: (id: string) => void) => () => void
+ onDeepLink: (cb: (urls: string[]) => void) => () => void
+
+ openDirectoryPicker: (opts?: {
+ multiple?: boolean
+ title?: string
+ defaultPath?: string
+ }) => Promise<string | string[] | null>
+ openFilePicker: (opts?: {
+ multiple?: boolean
+ title?: string
+ defaultPath?: string
+ }) => Promise<string | string[] | null>
+ saveFilePicker: (opts?: { title?: string; defaultPath?: string }) => Promise<string | null>
+ openLink: (url: string) => void
+ openPath: (path: string, app?: string) => Promise<void>
+ readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null>
+ showNotification: (title: string, body?: string) => void
+ getWindowFocused: () => Promise<boolean>
+ setWindowFocus: () => Promise<void>
+ showWindow: () => Promise<void>
+ relaunch: () => void
+ getZoomFactor: () => Promise<number>
+ setZoomFactor: (factor: number) => Promise<void>
+ loadingWindowComplete: () => void
+ runUpdater: (alertOnFail: boolean) => Promise<void>
+ checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
+ installUpdate: () => Promise<void>
+}
diff --git a/packages/desktop-electron/src/renderer/cli.ts b/packages/desktop-electron/src/renderer/cli.ts
new file mode 100644
index 000000000..11d3c1f1b
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/cli.ts
@@ -0,0 +1,12 @@
+import { initI18n, t } from "./i18n"
+
+export async function installCli(): Promise<void> {
+ await initI18n()
+
+ try {
+ const path = await window.api.installCli()
+ window.alert(t("desktop.cli.installed.message", { path }))
+ } catch (e) {
+ window.alert(t("desktop.cli.failed.message", { error: String(e) }))
+ }
+}
diff --git a/packages/desktop-electron/src/renderer/env.d.ts b/packages/desktop-electron/src/renderer/env.d.ts
new file mode 100644
index 000000000..d1590ff04
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/env.d.ts
@@ -0,0 +1,12 @@
+import type { ElectronAPI } from "../preload/types"
+
+declare global {
+ interface Window {
+ api: ElectronAPI
+ __OPENCODE__?: {
+ updaterEnabled?: boolean
+ wsl?: boolean
+ deepLinks?: string[]
+ }
+ }
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/ar.ts b/packages/desktop-electron/src/renderer/i18n/ar.ts
new file mode 100644
index 000000000..fdbf0a804
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/ar.ts
@@ -0,0 +1,26 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "التحقق من وجود تحديثات...",
+ "desktop.menu.installCli": "تثبيت CLI...",
+ "desktop.menu.reloadWebview": "إعادة تحميل Webview",
+ "desktop.menu.restart": "إعادة تشغيل",
+
+ "desktop.dialog.chooseFolder": "اختر مجلدًا",
+ "desktop.dialog.chooseFile": "اختر ملفًا",
+ "desktop.dialog.saveFile": "حفظ ملف",
+
+ "desktop.updater.checkFailed.title": "فشل التحقق من التحديثات",
+ "desktop.updater.checkFailed.message": "فشل التحقق من وجود تحديثات",
+ "desktop.updater.none.title": "لا توجد تحديثات متاحة",
+ "desktop.updater.none.message": "أنت تستخدم بالفعل أحدث إصدار من OpenCode",
+ "desktop.updater.downloadFailed.title": "فشل التحديث",
+ "desktop.updater.downloadFailed.message": "فشل تنزيل التحديث",
+ "desktop.updater.downloaded.title": "تم تنزيل التحديث",
+ "desktop.updater.downloaded.prompt": "تم تنزيل إصدار {{version}} من OpenCode، هل ترغب في تثبيته وإعادة تشغيله؟",
+ "desktop.updater.installFailed.title": "فشل التحديث",
+ "desktop.updater.installFailed.message": "فشل تثبيت التحديث",
+
+ "desktop.cli.installed.title": "تم تثبيت CLI",
+ "desktop.cli.installed.message": "تم تثبيت CLI في {{path}}\n\nأعد تشغيل الطرفية لاستخدام الأمر 'opencode'.",
+ "desktop.cli.failed.title": "فشل التثبيت",
+ "desktop.cli.failed.message": "فشل تثبيت CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/br.ts b/packages/desktop-electron/src/renderer/i18n/br.ts
new file mode 100644
index 000000000..75fe2dc32
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/br.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Verificar atualizações...",
+ "desktop.menu.installCli": "Instalar CLI...",
+ "desktop.menu.reloadWebview": "Recarregar Webview",
+ "desktop.menu.restart": "Reiniciar",
+
+ "desktop.dialog.chooseFolder": "Escolher uma pasta",
+ "desktop.dialog.chooseFile": "Escolher um arquivo",
+ "desktop.dialog.saveFile": "Salvar arquivo",
+
+ "desktop.updater.checkFailed.title": "Falha ao verificar atualizações",
+ "desktop.updater.checkFailed.message": "Falha ao verificar atualizações",
+ "desktop.updater.none.title": "Nenhuma atualização disponível",
+ "desktop.updater.none.message": "Você já está usando a versão mais recente do OpenCode",
+ "desktop.updater.downloadFailed.title": "Falha na atualização",
+ "desktop.updater.downloadFailed.message": "Falha ao baixar a atualização",
+ "desktop.updater.downloaded.title": "Atualização baixada",
+ "desktop.updater.downloaded.prompt":
+ "A versão {{version}} do OpenCode foi baixada. Você gostaria de instalá-la e reiniciar?",
+ "desktop.updater.installFailed.title": "Falha na atualização",
+ "desktop.updater.installFailed.message": "Falha ao instalar a atualização",
+
+ "desktop.cli.installed.title": "CLI instalada",
+ "desktop.cli.installed.message": "CLI instalada em {{path}}\n\nReinicie seu terminal para usar o comando 'opencode'.",
+ "desktop.cli.failed.title": "Falha na instalação",
+ "desktop.cli.failed.message": "Falha ao instalar a CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/bs.ts b/packages/desktop-electron/src/renderer/i18n/bs.ts
new file mode 100644
index 000000000..58c266f53
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/bs.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Provjeri ažuriranja...",
+ "desktop.menu.installCli": "Instaliraj CLI...",
+ "desktop.menu.reloadWebview": "Ponovo učitavanje webview-a",
+ "desktop.menu.restart": "Restartuj",
+
+ "desktop.dialog.chooseFolder": "Odaberi folder",
+ "desktop.dialog.chooseFile": "Odaberi datoteku",
+ "desktop.dialog.saveFile": "Sačuvaj datoteku",
+
+ "desktop.updater.checkFailed.title": "Provjera ažuriranja nije uspjela",
+ "desktop.updater.checkFailed.message": "Nije moguće provjeriti ažuriranja",
+ "desktop.updater.none.title": "Nema dostupnog ažuriranja",
+ "desktop.updater.none.message": "Već koristiš najnoviju verziju OpenCode-a",
+ "desktop.updater.downloadFailed.title": "Ažuriranje nije uspjelo",
+ "desktop.updater.downloadFailed.message": "Neuspjelo preuzimanje ažuriranja",
+ "desktop.updater.downloaded.title": "Ažuriranje preuzeto",
+ "desktop.updater.downloaded.prompt":
+ "Verzija {{version}} OpenCode-a je preuzeta. Želiš li da je instaliraš i ponovo pokreneš aplikaciju?",
+ "desktop.updater.installFailed.title": "Ažuriranje nije uspjelo",
+ "desktop.updater.installFailed.message": "Neuspjela instalacija ažuriranja",
+
+ "desktop.cli.installed.title": "CLI instaliran",
+ "desktop.cli.installed.message":
+ "CLI je instaliran u {{path}}\n\nRestartuj terminal da bi koristio komandu 'opencode'.",
+ "desktop.cli.failed.title": "Instalacija nije uspjela",
+ "desktop.cli.failed.message": "Neuspjela instalacija CLI-a: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/da.ts b/packages/desktop-electron/src/renderer/i18n/da.ts
new file mode 100644
index 000000000..2109495f7
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/da.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Tjek for opdateringer...",
+ "desktop.menu.installCli": "Installer CLI...",
+ "desktop.menu.reloadWebview": "Genindlæs Webview",
+ "desktop.menu.restart": "Genstart",
+
+ "desktop.dialog.chooseFolder": "Vælg en mappe",
+ "desktop.dialog.chooseFile": "Vælg en fil",
+ "desktop.dialog.saveFile": "Gem fil",
+
+ "desktop.updater.checkFailed.title": "Opdateringstjek mislykkedes",
+ "desktop.updater.checkFailed.message": "Kunne ikke tjekke for opdateringer",
+ "desktop.updater.none.title": "Ingen opdatering tilgængelig",
+ "desktop.updater.none.message": "Du bruger allerede den nyeste version af OpenCode",
+ "desktop.updater.downloadFailed.title": "Opdatering mislykkedes",
+ "desktop.updater.downloadFailed.message": "Kunne ikke downloade opdateringen",
+ "desktop.updater.downloaded.title": "Opdatering downloadet",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} af OpenCode er blevet downloadet. Vil du installere den og genstarte?",
+ "desktop.updater.installFailed.title": "Opdatering mislykkedes",
+ "desktop.updater.installFailed.message": "Kunne ikke installere opdateringen",
+
+ "desktop.cli.installed.title": "CLI installeret",
+ "desktop.cli.installed.message":
+ "CLI installeret i {{path}}\n\nGenstart din terminal for at bruge 'opencode'-kommandoen.",
+ "desktop.cli.failed.title": "Installation mislykkedes",
+ "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/de.ts b/packages/desktop-electron/src/renderer/i18n/de.ts
new file mode 100644
index 000000000..38ad8096e
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/de.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Nach Updates suchen...",
+ "desktop.menu.installCli": "CLI installieren...",
+ "desktop.menu.reloadWebview": "Webview neu laden",
+ "desktop.menu.restart": "Neustart",
+
+ "desktop.dialog.chooseFolder": "Ordner auswählen",
+ "desktop.dialog.chooseFile": "Datei auswählen",
+ "desktop.dialog.saveFile": "Datei speichern",
+
+ "desktop.updater.checkFailed.title": "Updateprüfung fehlgeschlagen",
+ "desktop.updater.checkFailed.message": "Updates konnten nicht geprüft werden",
+ "desktop.updater.none.title": "Kein Update verfügbar",
+ "desktop.updater.none.message": "Sie verwenden bereits die neueste Version von OpenCode",
+ "desktop.updater.downloadFailed.title": "Update fehlgeschlagen",
+ "desktop.updater.downloadFailed.message": "Update konnte nicht heruntergeladen werden",
+ "desktop.updater.downloaded.title": "Update heruntergeladen",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} von OpenCode wurde heruntergeladen. Möchten Sie sie installieren und neu starten?",
+ "desktop.updater.installFailed.title": "Update fehlgeschlagen",
+ "desktop.updater.installFailed.message": "Update konnte nicht installiert werden",
+
+ "desktop.cli.installed.title": "CLI installiert",
+ "desktop.cli.installed.message":
+ "CLI wurde in {{path}} installiert\n\nStarten Sie Ihr Terminal neu, um den Befehl 'opencode' zu verwenden.",
+ "desktop.cli.failed.title": "Installation fehlgeschlagen",
+ "desktop.cli.failed.message": "CLI konnte nicht installiert werden: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/en.ts b/packages/desktop-electron/src/renderer/i18n/en.ts
new file mode 100644
index 000000000..4c30380d5
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/en.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Check for Updates...",
+ "desktop.menu.installCli": "Install CLI...",
+ "desktop.menu.reloadWebview": "Reload Webview",
+ "desktop.menu.restart": "Restart",
+
+ "desktop.dialog.chooseFolder": "Choose a folder",
+ "desktop.dialog.chooseFile": "Choose a file",
+ "desktop.dialog.saveFile": "Save file",
+
+ "desktop.updater.checkFailed.title": "Update Check Failed",
+ "desktop.updater.checkFailed.message": "Failed to check for updates",
+ "desktop.updater.none.title": "No Update Available",
+ "desktop.updater.none.message": "You are already using the latest version of OpenCode",
+ "desktop.updater.downloadFailed.title": "Update Failed",
+ "desktop.updater.downloadFailed.message": "Failed to download update",
+ "desktop.updater.downloaded.title": "Update Downloaded",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} of OpenCode has been downloaded, would you like to install it and relaunch?",
+ "desktop.updater.installFailed.title": "Update Failed",
+ "desktop.updater.installFailed.message": "Failed to install update",
+
+ "desktop.cli.installed.title": "CLI Installed",
+ "desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode' command.",
+ "desktop.cli.failed.title": "Installation Failed",
+ "desktop.cli.failed.message": "Failed to install CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/es.ts b/packages/desktop-electron/src/renderer/i18n/es.ts
new file mode 100644
index 000000000..80504a8f2
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/es.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Buscar actualizaciones...",
+ "desktop.menu.installCli": "Instalar CLI...",
+ "desktop.menu.reloadWebview": "Recargar Webview",
+ "desktop.menu.restart": "Reiniciar",
+
+ "desktop.dialog.chooseFolder": "Elegir una carpeta",
+ "desktop.dialog.chooseFile": "Elegir un archivo",
+ "desktop.dialog.saveFile": "Guardar archivo",
+
+ "desktop.updater.checkFailed.title": "Comprobación de actualizaciones fallida",
+ "desktop.updater.checkFailed.message": "No se pudieron buscar actualizaciones",
+ "desktop.updater.none.title": "No hay actualizaciones disponibles",
+ "desktop.updater.none.message": "Ya estás usando la versión más reciente de OpenCode",
+ "desktop.updater.downloadFailed.title": "Actualización fallida",
+ "desktop.updater.downloadFailed.message": "No se pudo descargar la actualización",
+ "desktop.updater.downloaded.title": "Actualización descargada",
+ "desktop.updater.downloaded.prompt":
+ "Se ha descargado la versión {{version}} de OpenCode. ¿Quieres instalarla y reiniciar?",
+ "desktop.updater.installFailed.title": "Actualización fallida",
+ "desktop.updater.installFailed.message": "No se pudo instalar la actualización",
+
+ "desktop.cli.installed.title": "CLI instalada",
+ "desktop.cli.installed.message": "CLI instalada en {{path}}\n\nReinicia tu terminal para usar el comando 'opencode'.",
+ "desktop.cli.failed.title": "Instalación fallida",
+ "desktop.cli.failed.message": "No se pudo instalar la CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/fr.ts b/packages/desktop-electron/src/renderer/i18n/fr.ts
new file mode 100644
index 000000000..4f0bb2b16
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/fr.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Vérifier les mises à jour...",
+ "desktop.menu.installCli": "Installer la CLI...",
+ "desktop.menu.reloadWebview": "Recharger la Webview",
+ "desktop.menu.restart": "Redémarrer",
+
+ "desktop.dialog.chooseFolder": "Choisir un dossier",
+ "desktop.dialog.chooseFile": "Choisir un fichier",
+ "desktop.dialog.saveFile": "Enregistrer le fichier",
+
+ "desktop.updater.checkFailed.title": "Échec de la vérification des mises à jour",
+ "desktop.updater.checkFailed.message": "Impossible de vérifier les mises à jour",
+ "desktop.updater.none.title": "Aucune mise à jour disponible",
+ "desktop.updater.none.message": "Vous utilisez déjà la dernière version d'OpenCode",
+ "desktop.updater.downloadFailed.title": "Échec de la mise à jour",
+ "desktop.updater.downloadFailed.message": "Impossible de télécharger la mise à jour",
+ "desktop.updater.downloaded.title": "Mise à jour téléchargée",
+ "desktop.updater.downloaded.prompt":
+ "La version {{version}} d'OpenCode a été téléchargée. Voulez-vous l'installer et redémarrer ?",
+ "desktop.updater.installFailed.title": "Échec de la mise à jour",
+ "desktop.updater.installFailed.message": "Impossible d'installer la mise à jour",
+
+ "desktop.cli.installed.title": "CLI installée",
+ "desktop.cli.installed.message":
+ "CLI installée dans {{path}}\n\nRedémarrez votre terminal pour utiliser la commande 'opencode'.",
+ "desktop.cli.failed.title": "Échec de l'installation",
+ "desktop.cli.failed.message": "Impossible d'installer la CLI : {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/index.ts b/packages/desktop-electron/src/renderer/i18n/index.ts
new file mode 100644
index 000000000..81158ad24
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/index.ts
@@ -0,0 +1,187 @@
+import * as i18n from "@solid-primitives/i18n"
+
+import { dict as desktopEn } from "./en"
+import { dict as desktopZh } from "./zh"
+import { dict as desktopZht } from "./zht"
+import { dict as desktopKo } from "./ko"
+import { dict as desktopDe } from "./de"
+import { dict as desktopEs } from "./es"
+import { dict as desktopFr } from "./fr"
+import { dict as desktopDa } from "./da"
+import { dict as desktopJa } from "./ja"
+import { dict as desktopPl } from "./pl"
+import { dict as desktopRu } from "./ru"
+import { dict as desktopAr } from "./ar"
+import { dict as desktopNo } from "./no"
+import { dict as desktopBr } from "./br"
+import { dict as desktopBs } from "./bs"
+
+import { dict as appEn } from "../../../../app/src/i18n/en"
+import { dict as appZh } from "../../../../app/src/i18n/zh"
+import { dict as appZht } from "../../../../app/src/i18n/zht"
+import { dict as appKo } from "../../../../app/src/i18n/ko"
+import { dict as appDe } from "../../../../app/src/i18n/de"
+import { dict as appEs } from "../../../../app/src/i18n/es"
+import { dict as appFr } from "../../../../app/src/i18n/fr"
+import { dict as appDa } from "../../../../app/src/i18n/da"
+import { dict as appJa } from "../../../../app/src/i18n/ja"
+import { dict as appPl } from "../../../../app/src/i18n/pl"
+import { dict as appRu } from "../../../../app/src/i18n/ru"
+import { dict as appAr } from "../../../../app/src/i18n/ar"
+import { dict as appNo } from "../../../../app/src/i18n/no"
+import { dict as appBr } from "../../../../app/src/i18n/br"
+import { dict as appBs } from "../../../../app/src/i18n/bs"
+
+export type Locale =
+ | "en"
+ | "zh"
+ | "zht"
+ | "ko"
+ | "de"
+ | "es"
+ | "fr"
+ | "da"
+ | "ja"
+ | "pl"
+ | "ru"
+ | "ar"
+ | "no"
+ | "br"
+ | "bs"
+
+type RawDictionary = typeof appEn & typeof desktopEn
+type Dictionary = i18n.Flatten<RawDictionary>
+
+const LOCALES: readonly Locale[] = [
+ "en",
+ "zh",
+ "zht",
+ "ko",
+ "de",
+ "es",
+ "fr",
+ "da",
+ "ja",
+ "pl",
+ "ru",
+ "bs",
+ "ar",
+ "no",
+ "br",
+]
+
+function detectLocale(): Locale {
+ if (typeof navigator !== "object") return "en"
+
+ const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
+ for (const language of languages) {
+ if (!language) continue
+ if (language.toLowerCase().startsWith("zh")) {
+ if (language.toLowerCase().includes("hant")) return "zht"
+ return "zh"
+ }
+ if (language.toLowerCase().startsWith("ko")) return "ko"
+ if (language.toLowerCase().startsWith("de")) return "de"
+ if (language.toLowerCase().startsWith("es")) return "es"
+ if (language.toLowerCase().startsWith("fr")) return "fr"
+ if (language.toLowerCase().startsWith("da")) return "da"
+ if (language.toLowerCase().startsWith("ja")) return "ja"
+ if (language.toLowerCase().startsWith("pl")) return "pl"
+ if (language.toLowerCase().startsWith("ru")) return "ru"
+ if (language.toLowerCase().startsWith("ar")) return "ar"
+ if (
+ language.toLowerCase().startsWith("no") ||
+ language.toLowerCase().startsWith("nb") ||
+ language.toLowerCase().startsWith("nn")
+ )
+ return "no"
+ if (language.toLowerCase().startsWith("pt")) return "br"
+ if (language.toLowerCase().startsWith("bs")) return "bs"
+ }
+
+ return "en"
+}
+
+function parseLocale(value: unknown): Locale | null {
+ if (!value) return null
+ if (typeof value !== "string") return null
+ if ((LOCALES as readonly string[]).includes(value)) return value as Locale
+ return null
+}
+
+function parseRecord(value: unknown) {
+ if (!value || typeof value !== "object") return null
+ if (Array.isArray(value)) return null
+ return value as Record<string, unknown>
+}
+
+function parseStored(value: unknown) {
+ if (typeof value !== "string") return value
+ try {
+ return JSON.parse(value) as unknown
+ } catch {
+ return value
+ }
+}
+
+function pickLocale(value: unknown): Locale | null {
+ const direct = parseLocale(value)
+ if (direct) return direct
+
+ const record = parseRecord(value)
+ if (!record) return null
+
+ return parseLocale(record.locale)
+}
+
+const base = i18n.flatten({ ...appEn, ...desktopEn })
+
+function build(locale: Locale): Dictionary {
+ if (locale === "en") return base
+ if (locale === "zh") return { ...base, ...i18n.flatten(appZh), ...i18n.flatten(desktopZh) }
+ if (locale === "zht") return { ...base, ...i18n.flatten(appZht), ...i18n.flatten(desktopZht) }
+ if (locale === "de") return { ...base, ...i18n.flatten(appDe), ...i18n.flatten(desktopDe) }
+ if (locale === "es") return { ...base, ...i18n.flatten(appEs), ...i18n.flatten(desktopEs) }
+ if (locale === "fr") return { ...base, ...i18n.flatten(appFr), ...i18n.flatten(desktopFr) }
+ if (locale === "da") return { ...base, ...i18n.flatten(appDa), ...i18n.flatten(desktopDa) }
+ if (locale === "ja") return { ...base, ...i18n.flatten(appJa), ...i18n.flatten(desktopJa) }
+ if (locale === "pl") return { ...base, ...i18n.flatten(appPl), ...i18n.flatten(desktopPl) }
+ if (locale === "ru") return { ...base, ...i18n.flatten(appRu), ...i18n.flatten(desktopRu) }
+ if (locale === "ar") return { ...base, ...i18n.flatten(appAr), ...i18n.flatten(desktopAr) }
+ if (locale === "no") return { ...base, ...i18n.flatten(appNo), ...i18n.flatten(desktopNo) }
+ if (locale === "br") return { ...base, ...i18n.flatten(appBr), ...i18n.flatten(desktopBr) }
+ if (locale === "bs") return { ...base, ...i18n.flatten(appBs), ...i18n.flatten(desktopBs) }
+ return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) }
+}
+
+const state = {
+ locale: detectLocale(),
+ dict: base as Dictionary,
+ init: undefined as Promise<Locale> | undefined,
+}
+
+state.dict = build(state.locale)
+
+const translate = i18n.translator(() => state.dict, i18n.resolveTemplate)
+
+export function t(key: keyof Dictionary, params?: Record<string, string | number>) {
+ return translate(key, params)
+}
+
+export function initI18n(): Promise<Locale> {
+ const cached = state.init
+ if (cached) return cached
+
+ const promise = (async () => {
+ const raw = await window.api.storeGet("opencode.global.dat", "language").catch(() => null)
+ const value = parseStored(raw)
+ const next = pickLocale(value) ?? state.locale
+
+ state.locale = next
+ state.dict = build(next)
+ return next
+ })().catch(() => state.locale)
+
+ state.init = promise
+ return promise
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/ja.ts b/packages/desktop-electron/src/renderer/i18n/ja.ts
new file mode 100644
index 000000000..fc485c6f4
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/ja.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "アップデートを確認...",
+ "desktop.menu.installCli": "CLI をインストール...",
+ "desktop.menu.reloadWebview": "Webview を再読み込み",
+ "desktop.menu.restart": "再起動",
+
+ "desktop.dialog.chooseFolder": "フォルダーを選択",
+ "desktop.dialog.chooseFile": "ファイルを選択",
+ "desktop.dialog.saveFile": "ファイルを保存",
+
+ "desktop.updater.checkFailed.title": "アップデートの確認に失敗しました",
+ "desktop.updater.checkFailed.message": "アップデートを確認できませんでした",
+ "desktop.updater.none.title": "利用可能なアップデートはありません",
+ "desktop.updater.none.message": "すでに最新バージョンの OpenCode を使用しています",
+ "desktop.updater.downloadFailed.title": "アップデートに失敗しました",
+ "desktop.updater.downloadFailed.message": "アップデートをダウンロードできませんでした",
+ "desktop.updater.downloaded.title": "アップデートをダウンロードしました",
+ "desktop.updater.downloaded.prompt":
+ "OpenCode のバージョン {{version}} がダウンロードされました。インストールして再起動しますか?",
+ "desktop.updater.installFailed.title": "アップデートに失敗しました",
+ "desktop.updater.installFailed.message": "アップデートをインストールできませんでした",
+
+ "desktop.cli.installed.title": "CLI をインストールしました",
+ "desktop.cli.installed.message":
+ "CLI を {{path}} にインストールしました\n\nターミナルを再起動して 'opencode' コマンドを使用してください。",
+ "desktop.cli.failed.title": "インストールに失敗しました",
+ "desktop.cli.failed.message": "CLI のインストールに失敗しました: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/ko.ts b/packages/desktop-electron/src/renderer/i18n/ko.ts
new file mode 100644
index 000000000..be27cec86
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/ko.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "업데이트 확인...",
+ "desktop.menu.installCli": "CLI 설치...",
+ "desktop.menu.reloadWebview": "Webview 새로고침",
+ "desktop.menu.restart": "다시 시작",
+
+ "desktop.dialog.chooseFolder": "폴더 선택",
+ "desktop.dialog.chooseFile": "파일 선택",
+ "desktop.dialog.saveFile": "파일 저장",
+
+ "desktop.updater.checkFailed.title": "업데이트 확인 실패",
+ "desktop.updater.checkFailed.message": "업데이트를 확인하지 못했습니다",
+ "desktop.updater.none.title": "사용 가능한 업데이트 없음",
+ "desktop.updater.none.message": "이미 최신 버전의 OpenCode를 사용하고 있습니다",
+ "desktop.updater.downloadFailed.title": "업데이트 실패",
+ "desktop.updater.downloadFailed.message": "업데이트를 다운로드하지 못했습니다",
+ "desktop.updater.downloaded.title": "업데이트 다운로드 완료",
+ "desktop.updater.downloaded.prompt": "OpenCode {{version}} 버전을 다운로드했습니다. 설치하고 다시 실행할까요?",
+ "desktop.updater.installFailed.title": "업데이트 실패",
+ "desktop.updater.installFailed.message": "업데이트를 설치하지 못했습니다",
+
+ "desktop.cli.installed.title": "CLI 설치됨",
+ "desktop.cli.installed.message":
+ "CLI가 {{path}}에 설치되었습니다\n\n터미널을 다시 시작하여 'opencode' 명령을 사용하세요.",
+ "desktop.cli.failed.title": "설치 실패",
+ "desktop.cli.failed.message": "CLI 설치 실패: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/no.ts b/packages/desktop-electron/src/renderer/i18n/no.ts
new file mode 100644
index 000000000..e39bd7f3b
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/no.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Se etter oppdateringer...",
+ "desktop.menu.installCli": "Installer CLI...",
+ "desktop.menu.reloadWebview": "Last inn Webview på nytt",
+ "desktop.menu.restart": "Start på nytt",
+
+ "desktop.dialog.chooseFolder": "Velg en mappe",
+ "desktop.dialog.chooseFile": "Velg en fil",
+ "desktop.dialog.saveFile": "Lagre fil",
+
+ "desktop.updater.checkFailed.title": "Oppdateringssjekk mislyktes",
+ "desktop.updater.checkFailed.message": "Kunne ikke se etter oppdateringer",
+ "desktop.updater.none.title": "Ingen oppdatering tilgjengelig",
+ "desktop.updater.none.message": "Du bruker allerede den nyeste versjonen av OpenCode",
+ "desktop.updater.downloadFailed.title": "Oppdatering mislyktes",
+ "desktop.updater.downloadFailed.message": "Kunne ikke laste ned oppdateringen",
+ "desktop.updater.downloaded.title": "Oppdatering lastet ned",
+ "desktop.updater.downloaded.prompt":
+ "Versjon {{version}} av OpenCode er lastet ned. Vil du installere den og starte på nytt?",
+ "desktop.updater.installFailed.title": "Oppdatering mislyktes",
+ "desktop.updater.installFailed.message": "Kunne ikke installere oppdateringen",
+
+ "desktop.cli.installed.title": "CLI installert",
+ "desktop.cli.installed.message":
+ "CLI installert til {{path}}\n\nStart terminalen på nytt for å bruke 'opencode'-kommandoen.",
+ "desktop.cli.failed.title": "Installasjon mislyktes",
+ "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/pl.ts b/packages/desktop-electron/src/renderer/i18n/pl.ts
new file mode 100644
index 000000000..d3ad7ce64
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/pl.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Sprawdź aktualizacje...",
+ "desktop.menu.installCli": "Zainstaluj CLI...",
+ "desktop.menu.reloadWebview": "Przeładuj Webview",
+ "desktop.menu.restart": "Restartuj",
+
+ "desktop.dialog.chooseFolder": "Wybierz folder",
+ "desktop.dialog.chooseFile": "Wybierz plik",
+ "desktop.dialog.saveFile": "Zapisz plik",
+
+ "desktop.updater.checkFailed.title": "Nie udało się sprawdzić aktualizacji",
+ "desktop.updater.checkFailed.message": "Nie udało się sprawdzić aktualizacji",
+ "desktop.updater.none.title": "Brak dostępnych aktualizacji",
+ "desktop.updater.none.message": "Korzystasz już z najnowszej wersji OpenCode",
+ "desktop.updater.downloadFailed.title": "Aktualizacja nie powiodła się",
+ "desktop.updater.downloadFailed.message": "Nie udało się pobrać aktualizacji",
+ "desktop.updater.downloaded.title": "Aktualizacja pobrana",
+ "desktop.updater.downloaded.prompt":
+ "Pobrano wersję {{version}} OpenCode. Czy chcesz ją zainstalować i uruchomić ponownie?",
+ "desktop.updater.installFailed.title": "Aktualizacja nie powiodła się",
+ "desktop.updater.installFailed.message": "Nie udało się zainstalować aktualizacji",
+
+ "desktop.cli.installed.title": "CLI zainstalowane",
+ "desktop.cli.installed.message":
+ "CLI zainstalowane w {{path}}\n\nUruchom ponownie terminal, aby użyć polecenia 'opencode'.",
+ "desktop.cli.failed.title": "Instalacja nie powiodła się",
+ "desktop.cli.failed.message": "Nie udało się zainstalować CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/ru.ts b/packages/desktop-electron/src/renderer/i18n/ru.ts
new file mode 100644
index 000000000..8e09cc45b
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/ru.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Проверить обновления...",
+ "desktop.menu.installCli": "Установить CLI...",
+ "desktop.menu.reloadWebview": "Перезагрузить Webview",
+ "desktop.menu.restart": "Перезапустить",
+
+ "desktop.dialog.chooseFolder": "Выберите папку",
+ "desktop.dialog.chooseFile": "Выберите файл",
+ "desktop.dialog.saveFile": "Сохранить файл",
+
+ "desktop.updater.checkFailed.title": "Не удалось проверить обновления",
+ "desktop.updater.checkFailed.message": "Не удалось проверить обновления",
+ "desktop.updater.none.title": "Обновлений нет",
+ "desktop.updater.none.message": "Вы уже используете последнюю версию OpenCode",
+ "desktop.updater.downloadFailed.title": "Обновление не удалось",
+ "desktop.updater.downloadFailed.message": "Не удалось скачать обновление",
+ "desktop.updater.downloaded.title": "Обновление загружено",
+ "desktop.updater.downloaded.prompt": "Версия OpenCode {{version}} загружена. Хотите установить и перезапустить?",
+ "desktop.updater.installFailed.title": "Обновление не удалось",
+ "desktop.updater.installFailed.message": "Не удалось установить обновление",
+
+ "desktop.cli.installed.title": "CLI установлен",
+ "desktop.cli.installed.message":
+ "CLI установлен в {{path}}\n\nПерезапустите терминал, чтобы использовать команду 'opencode'.",
+ "desktop.cli.failed.title": "Ошибка установки",
+ "desktop.cli.failed.message": "Не удалось установить CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/zh.ts b/packages/desktop-electron/src/renderer/i18n/zh.ts
new file mode 100644
index 000000000..aeb3a54e0
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/zh.ts
@@ -0,0 +1,26 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "检查更新...",
+ "desktop.menu.installCli": "安装 CLI...",
+ "desktop.menu.reloadWebview": "重新加载 Webview",
+ "desktop.menu.restart": "重启",
+
+ "desktop.dialog.chooseFolder": "选择文件夹",
+ "desktop.dialog.chooseFile": "选择文件",
+ "desktop.dialog.saveFile": "保存文件",
+
+ "desktop.updater.checkFailed.title": "检查更新失败",
+ "desktop.updater.checkFailed.message": "无法检查更新",
+ "desktop.updater.none.title": "没有可用更新",
+ "desktop.updater.none.message": "你已经在使用最新版本的 OpenCode",
+ "desktop.updater.downloadFailed.title": "更新失败",
+ "desktop.updater.downloadFailed.message": "无法下载更新",
+ "desktop.updater.downloaded.title": "更新已下载",
+ "desktop.updater.downloaded.prompt": "已下载 OpenCode {{version}} 版本,是否安装并重启?",
+ "desktop.updater.installFailed.title": "更新失败",
+ "desktop.updater.installFailed.message": "无法安装更新",
+
+ "desktop.cli.installed.title": "CLI 已安装",
+ "desktop.cli.installed.message": "CLI 已安装到 {{path}}\n\n重启终端以使用 'opencode' 命令。",
+ "desktop.cli.failed.title": "安装失败",
+ "desktop.cli.failed.message": "无法安装 CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/zht.ts b/packages/desktop-electron/src/renderer/i18n/zht.ts
new file mode 100644
index 000000000..7fd677aca
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/zht.ts
@@ -0,0 +1,26 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "檢查更新...",
+ "desktop.menu.installCli": "安裝 CLI...",
+ "desktop.menu.reloadWebview": "重新載入 Webview",
+ "desktop.menu.restart": "重新啟動",
+
+ "desktop.dialog.chooseFolder": "選擇資料夾",
+ "desktop.dialog.chooseFile": "選擇檔案",
+ "desktop.dialog.saveFile": "儲存檔案",
+
+ "desktop.updater.checkFailed.title": "檢查更新失敗",
+ "desktop.updater.checkFailed.message": "無法檢查更新",
+ "desktop.updater.none.title": "沒有可用更新",
+ "desktop.updater.none.message": "你已在使用最新版的 OpenCode",
+ "desktop.updater.downloadFailed.title": "更新失敗",
+ "desktop.updater.downloadFailed.message": "無法下載更新",
+ "desktop.updater.downloaded.title": "更新已下載",
+ "desktop.updater.downloaded.prompt": "已下載 OpenCode {{version}} 版本,是否安裝並重新啟動?",
+ "desktop.updater.installFailed.title": "更新失敗",
+ "desktop.updater.installFailed.message": "無法安裝更新",
+
+ "desktop.cli.installed.title": "CLI 已安裝",
+ "desktop.cli.installed.message": "CLI 已安裝到 {{path}}\n\n重新啟動終端機以使用 'opencode' 命令。",
+ "desktop.cli.failed.title": "安裝失敗",
+ "desktop.cli.failed.message": "無法安裝 CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/index.html b/packages/desktop-electron/src/renderer/index.html
new file mode 100644
index 000000000..175640819
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/index.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en" style="background-color: var(--background-base)">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>OpenCode</title>
+ <link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
+ <link rel="icon" type="image/svg+xml" href="/favicon-v3.svg" />
+ <link rel="shortcut icon" href="/favicon-v3.ico" />
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
+ <link rel="manifest" href="/site.webmanifest" />
+ <meta name="theme-color" content="#F8F7F7" />
+ <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
+ <meta property="og:image" content="/social-share.png" />
+ <meta property="twitter:image" content="/social-share.png" />
+ <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
+ </head>
+ <body class="antialiased overscroll-none text-12-regular overflow-hidden">
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ <div id="root" class="flex flex-col h-dvh"></div>
+ <script src="/index.tsx" type="module"></script>
+ </body>
+</html>
diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx
new file mode 100644
index 000000000..b5193d626
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/index.tsx
@@ -0,0 +1,312 @@
+// @refresh reload
+
+import {
+ AppBaseProviders,
+ AppInterface,
+ handleNotificationClick,
+ type Platform,
+ PlatformProvider,
+ ServerConnection,
+ useCommand,
+} from "@opencode-ai/app"
+import { Splash } from "@opencode-ai/ui/logo"
+import type { AsyncStorage } from "@solid-primitives/storage"
+import { type Accessor, createResource, type JSX, onCleanup, onMount, Show } from "solid-js"
+import { render } from "solid-js/web"
+import { MemoryRouter } from "@solidjs/router"
+import pkg from "../../package.json"
+import { initI18n, t } from "./i18n"
+import { UPDATER_ENABLED } from "./updater"
+import { webviewZoom } from "./webview-zoom"
+import "./styles.css"
+import type { ServerReadyData } from "../preload/types"
+
+const root = document.getElementById("root")
+if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
+ throw new Error(t("error.dev.rootNotFound"))
+}
+
+void initI18n()
+
+const deepLinkEvent = "opencode:deep-link"
+
+const emitDeepLinks = (urls: string[]) => {
+ if (urls.length === 0) return
+ window.__OPENCODE__ ??= {}
+ const pending = window.__OPENCODE__.deepLinks ?? []
+ window.__OPENCODE__.deepLinks = [...pending, ...urls]
+ window.dispatchEvent(new CustomEvent(deepLinkEvent, { detail: { urls } }))
+}
+
+const listenForDeepLinks = () => {
+ const startUrls = window.__OPENCODE__?.deepLinks ?? []
+ if (startUrls.length) emitDeepLinks(startUrls)
+ return window.api.onDeepLink((urls) => emitDeepLinks(urls))
+}
+
+const createPlatform = (): Platform => {
+ const os = (() => {
+ const ua = navigator.userAgent
+ if (ua.includes("Mac")) return "macos"
+ if (ua.includes("Windows")) return "windows"
+ if (ua.includes("Linux")) return "linux"
+ return undefined
+ })()
+
+ const wslHome = async () => {
+ if (os !== "windows" || !window.__OPENCODE__?.wsl) return undefined
+ return window.api.wslPath("~", "windows").catch(() => undefined)
+ }
+
+ const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
+ if (!result || !window.__OPENCODE__?.wsl) return result
+ if (Array.isArray(result)) {
+ return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any
+ }
+ return window.api.wslPath(result, "linux").catch(() => result) as any
+ }
+
+ const storage = (() => {
+ const cache = new Map<string, AsyncStorage>()
+
+ const createStorage = (name: string) => {
+ const api: AsyncStorage = {
+ getItem: (key: string) => window.api.storeGet(name, key),
+ setItem: (key: string, value: string) => window.api.storeSet(name, key, value),
+ removeItem: (key: string) => window.api.storeDelete(name, key),
+ clear: () => window.api.storeClear(name),
+ key: async (index: number) => (await window.api.storeKeys(name))[index],
+ getLength: () => window.api.storeLength(name),
+ get length() {
+ return api.getLength()
+ },
+ }
+ return api
+ }
+
+ return (name = "default.dat") => {
+ const cached = cache.get(name)
+ if (cached) return cached
+ const api = createStorage(name)
+ cache.set(name, api)
+ return api
+ }
+ })()
+
+ return {
+ platform: "desktop",
+ os,
+ version: pkg.version,
+
+ async openDirectoryPickerDialog(opts) {
+ const defaultPath = await wslHome()
+ const result = await window.api.openDirectoryPicker({
+ multiple: opts?.multiple ?? false,
+ title: opts?.title ?? t("desktop.dialog.chooseFolder"),
+ defaultPath,
+ })
+ return await handleWslPicker(result)
+ },
+
+ async openFilePickerDialog(opts) {
+ const result = await window.api.openFilePicker({
+ multiple: opts?.multiple ?? false,
+ title: opts?.title ?? t("desktop.dialog.chooseFile"),
+ })
+ return handleWslPicker(result)
+ },
+
+ async saveFilePickerDialog(opts) {
+ const result = await window.api.saveFilePicker({
+ title: opts?.title ?? t("desktop.dialog.saveFile"),
+ defaultPath: opts?.defaultPath,
+ })
+ return handleWslPicker(result)
+ },
+
+ openLink(url: string) {
+ window.api.openLink(url)
+ },
+ async openPath(path: string, app?: string) {
+ if (os === "windows") {
+ const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
+ const resolvedPath = await (async () => {
+ if (window.__OPENCODE__?.wsl) {
+ const converted = await window.api.wslPath(path, "windows").catch(() => null)
+ if (converted) return converted
+ }
+ return path
+ })()
+ return window.api.openPath(resolvedPath, resolvedApp ?? undefined)
+ }
+ return window.api.openPath(path, app)
+ },
+
+ back() {
+ window.history.back()
+ },
+
+ forward() {
+ window.history.forward()
+ },
+
+ storage,
+
+ checkUpdate: async () => {
+ if (!UPDATER_ENABLED) return { updateAvailable: false }
+ return window.api.checkUpdate()
+ },
+
+ update: async () => {
+ if (!UPDATER_ENABLED) return
+ await window.api.installUpdate()
+ },
+
+ restart: async () => {
+ await window.api.killSidecar().catch(() => undefined)
+ window.api.relaunch()
+ },
+
+ notify: async (title, description, href) => {
+ const focused = await window.api.getWindowFocused().catch(() => document.hasFocus())
+ if (focused) return
+
+ const notification = new Notification(title, {
+ body: description ?? "",
+ icon: "https://opencode.ai/favicon-96x96-v3.png",
+ })
+ notification.onclick = () => {
+ void window.api.showWindow()
+ void window.api.setWindowFocus()
+ handleNotificationClick(href)
+ notification.close()
+ }
+ },
+
+ fetch: (input, init) => {
+ if (input instanceof Request) return fetch(input)
+ return fetch(input, init)
+ },
+
+ getWslEnabled: async () => {
+ const next = await window.api.getWslConfig().catch(() => null)
+ if (next) return next.enabled
+ return window.__OPENCODE__!.wsl ?? false
+ },
+
+ setWslEnabled: async (enabled) => {
+ await window.api.setWslConfig({ enabled })
+ },
+
+ getDefaultServerUrl: async () => {
+ return window.api.getDefaultServerUrl().catch(() => null)
+ },
+
+ setDefaultServerUrl: async (url: string | null) => {
+ await window.api.setDefaultServerUrl(url)
+ },
+
+ getDisplayBackend: async () => {
+ return window.api.getDisplayBackend().catch(() => null)
+ },
+
+ setDisplayBackend: async (backend) => {
+ await window.api.setDisplayBackend(backend)
+ },
+
+ parseMarkdown: (markdown: string) => window.api.parseMarkdownCommand(markdown),
+
+ webviewZoom,
+
+ checkAppExists: async (appName: string) => {
+ return window.api.checkAppExists(appName)
+ },
+
+ async readClipboardImage() {
+ const image = await window.api.readClipboardImage().catch(() => null)
+ if (!image) return null
+ const blob = new Blob([image.buffer], { type: "image/png" })
+ return new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png" })
+ },
+ }
+}
+
+let menuTrigger = null as null | ((id: string) => void)
+window.api.onMenuCommand((id) => {
+ menuTrigger?.(id)
+})
+listenForDeepLinks()
+
+render(() => {
+ const platform = createPlatform()
+
+ function handleClick(e: MouseEvent) {
+ const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
+ if (link?.href) {
+ e.preventDefault()
+ platform.openLink(link.href)
+ }
+ }
+
+ onMount(() => {
+ document.addEventListener("click", handleClick)
+ onCleanup(() => {
+ document.removeEventListener("click", handleClick)
+ })
+ })
+
+ return (
+ <PlatformProvider value={platform}>
+ <AppBaseProviders>
+ <ServerGate>
+ {(data) => {
+ const server: ServerConnection.Sidecar = {
+ displayName: "Local Server",
+ type: "sidecar",
+ variant: "base",
+ http: {
+ url: data().url,
+ username: "opencode",
+ password: data().password ?? undefined,
+ },
+ }
+
+ function Inner() {
+ const cmd = useCommand()
+
+ menuTrigger = (id) => cmd.trigger(id)
+
+ return null
+ }
+
+ return (
+ <AppInterface defaultServer={ServerConnection.key(server)} servers={[server]} router={MemoryRouter}>
+ <Inner />
+ </AppInterface>
+ )
+ }}
+ </ServerGate>
+ </AppBaseProviders>
+ </PlatformProvider>
+ )
+}, root!)
+
+// Gate component that waits for the server to be ready
+function ServerGate(props: { children: (data: Accessor<ServerReadyData>) => JSX.Element }) {
+ const [serverData] = createResource(() => window.api.awaitInitialization(() => undefined))
+ console.log({ serverData })
+ if (serverData.state === "errored") throw serverData.error
+
+ return (
+ <Show
+ when={serverData.state !== "pending" && serverData()}
+ fallback={
+ <div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
+ <Splash class="w-16 h-20 opacity-50 animate-pulse" />
+ </div>
+ }
+ >
+ {(data) => props.children(data)}
+ </Show>
+ )
+}
diff --git a/packages/desktop-electron/src/renderer/loading.html b/packages/desktop-electron/src/renderer/loading.html
new file mode 100644
index 000000000..8def243b4
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/loading.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en" style="background-color: var(--background-base)">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>OpenCode</title>
+ <link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
+ <link rel="icon" type="image/svg+xml" href="/favicon-v3.svg" />
+ <link rel="shortcut icon" href="/favicon-v3.ico" />
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
+ <link rel="manifest" href="/site.webmanifest" />
+ <meta name="theme-color" content="#F8F7F7" />
+ <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
+ <meta property="og:image" content="/social-share.png" />
+ <meta property="twitter:image" content="/social-share.png" />
+ <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
+ </head>
+ <body class="antialiased overscroll-none text-12-regular overflow-hidden">
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ <div id="root" class="flex flex-col h-dvh"></div>
+ <script src="/loading.tsx" type="module"></script>
+ </body>
+</html>
diff --git a/packages/desktop-electron/src/renderer/loading.tsx b/packages/desktop-electron/src/renderer/loading.tsx
new file mode 100644
index 000000000..165950352
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/loading.tsx
@@ -0,0 +1,80 @@
+import { render } from "solid-js/web"
+import { MetaProvider } from "@solidjs/meta"
+import "@opencode-ai/app/index.css"
+import { Font } from "@opencode-ai/ui/font"
+import { Splash } from "@opencode-ai/ui/logo"
+import { Progress } from "@opencode-ai/ui/progress"
+import "./styles.css"
+import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
+import type { InitStep, SqliteMigrationProgress } from "../preload/types"
+
+const root = document.getElementById("root")!
+const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"]
+const delays = [3000, 9000]
+
+render(() => {
+ const [step, setStep] = createSignal<InitStep | null>(null)
+ const [line, setLine] = createSignal(0)
+ const [percent, setPercent] = createSignal(0)
+
+ const phase = createMemo(() => step()?.phase)
+
+ const value = createMemo(() => {
+ if (phase() === "done") return 100
+ return Math.max(25, Math.min(100, percent()))
+ })
+
+ window.api.awaitInitialization((next) => setStep(next as InitStep)).catch(() => undefined)
+
+ onMount(() => {
+ setLine(0)
+ setPercent(0)
+
+ const timers = delays.map((ms, i) => setTimeout(() => setLine(i + 1), ms))
+
+ const listener = window.api.onSqliteMigrationProgress((progress: SqliteMigrationProgress) => {
+ if (progress.type === "InProgress") setPercent(Math.max(0, Math.min(100, progress.value)))
+ if (progress.type === "Done") setPercent(100)
+ })
+
+ onCleanup(() => {
+ listener()
+ timers.forEach(clearTimeout)
+ })
+ })
+
+ createEffect(() => {
+ if (phase() !== "done") return
+
+ const timer = setTimeout(() => window.api.loadingWindowComplete(), 1000)
+ onCleanup(() => clearTimeout(timer))
+ })
+
+ const status = createMemo(() => {
+ if (phase() === "done") return "All done"
+ if (phase() === "sqlite_waiting") return lines[line()]
+ return "Just a moment..."
+ })
+
+ return (
+ <MetaProvider>
+ <div class="w-screen h-screen bg-background-base flex items-center justify-center">
+ <Font />
+ <div class="flex flex-col items-center gap-11">
+ <Splash class="w-20 h-25 opacity-15" />
+ <div class="w-60 flex flex-col items-center gap-4" aria-live="polite">
+ <span class="w-full overflow-hidden text-center text-ellipsis whitespace-nowrap text-text-strong text-14-normal">
+ {status()}
+ </span>
+ <Progress
+ value={value()}
+ class="w-20 [&_[data-slot='progress-track']]:h-1 [&_[data-slot='progress-track']]:border-0 [&_[data-slot='progress-track']]:rounded-none [&_[data-slot='progress-track']]:bg-surface-weak [&_[data-slot='progress-fill']]:rounded-none [&_[data-slot='progress-fill']]:bg-icon-warning-base"
+ aria-label="Database migration progress"
+ getValueLabel={({ value }) => `${Math.round(value)}%`}
+ />
+ </div>
+ </div>
+ </div>
+ </MetaProvider>
+ )
+}, root)
diff --git a/packages/desktop-electron/src/renderer/styles.css b/packages/desktop-electron/src/renderer/styles.css
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/styles.css
diff --git a/packages/desktop-electron/src/renderer/updater.ts b/packages/desktop-electron/src/renderer/updater.ts
new file mode 100644
index 000000000..fe9e601db
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/updater.ts
@@ -0,0 +1,14 @@
+import { initI18n, t } from "./i18n"
+
+export const UPDATER_ENABLED = window.__OPENCODE__?.updaterEnabled ?? false
+
+export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
+ await initI18n()
+ try {
+ await window.api.runUpdater(alertOnFail)
+ } catch {
+ if (alertOnFail) {
+ window.alert(t("desktop.updater.checkFailed.message"))
+ }
+ }
+}
diff --git a/packages/desktop-electron/src/renderer/webview-zoom.ts b/packages/desktop-electron/src/renderer/webview-zoom.ts
new file mode 100644
index 000000000..9c0a3a3a3
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/webview-zoom.ts
@@ -0,0 +1,38 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+import { createSignal } from "solid-js"
+
+const OS_NAME = (() => {
+ if (navigator.userAgent.includes("Mac")) return "macos"
+ if (navigator.userAgent.includes("Windows")) return "windows"
+ if (navigator.userAgent.includes("Linux")) return "linux"
+ return "unknown"
+})()
+
+const [webviewZoom, setWebviewZoom] = createSignal(1)
+
+const MAX_ZOOM_LEVEL = 10
+const MIN_ZOOM_LEVEL = 0.2
+
+const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
+
+const applyZoom = (next: number) => {
+ setWebviewZoom(next)
+ void window.api.setZoomFactor(next)
+}
+
+window.addEventListener("keydown", (event) => {
+ if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return
+
+ let newZoom = webviewZoom()
+
+ if (event.key === "-") newZoom -= 0.2
+ if (event.key === "=" || event.key === "+") newZoom += 0.2
+ if (event.key === "0") newZoom = 1
+
+ applyZoom(clamp(newZoom))
+})
+
+export { webviewZoom }
diff --git a/packages/desktop-electron/sst-env.d.ts b/packages/desktop-electron/sst-env.d.ts
new file mode 100644
index 000000000..64441936d
--- /dev/null
+++ b/packages/desktop-electron/sst-env.d.ts
@@ -0,0 +1,10 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+/* biome-ignore-all lint: auto-generated */
+
+/// <reference path="../../sst-env.d.ts" />
+
+import "sst"
+export {} \ No newline at end of file
diff --git a/packages/desktop-electron/tsconfig.json b/packages/desktop-electron/tsconfig.json
new file mode 100644
index 000000000..160f6c3fd
--- /dev/null
+++ b/packages/desktop-electron/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "allowJs": true,
+ "resolveJsonModule": true,
+ "strict": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "emitDeclarationOnly": false,
+ "outDir": "node_modules/.ts-dist",
+ "types": ["vite/client", "node", "electron"]
+ },
+ "references": [{ "path": "../app" }],
+ "include": ["src", "package.json"]
+}
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 4fe999e70..30d1b9405 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
- "version": "1.2.15",
+ "version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json
index cc46f7530..66509f6de 100644
--- a/packages/enterprise/package.json
+++ b/packages/enterprise/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
- "version": "1.2.15",
+ "version": "1.2.17",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml
index e9f246af8..622066414 100644
--- a/packages/extensions/zed/extension.toml
+++ b/packages/extensions/zed/extension.toml
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
-version = "1.2.15"
+version = "1.2.17"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-darwin-arm64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-darwin-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-linux-x64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-windows-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
diff --git a/packages/function/package.json b/packages/function/package.json
index 63e50b992..3d8606980 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "1.2.15",
+ "version": "1.2.17",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql b/packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql
new file mode 100644
index 000000000..185de5913
--- /dev/null
+++ b/packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql
@@ -0,0 +1,5 @@
+ALTER TABLE `workspace` ADD `type` text NOT NULL;--> statement-breakpoint
+ALTER TABLE `workspace` ADD `name` text;--> statement-breakpoint
+ALTER TABLE `workspace` ADD `directory` text;--> statement-breakpoint
+ALTER TABLE `workspace` ADD `extra` text;--> statement-breakpoint
+ALTER TABLE `workspace` DROP COLUMN `config`; \ No newline at end of file
diff --git a/packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json b/packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json
new file mode 100644
index 000000000..4fe320a2c
--- /dev/null
+++ b/packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json
@@ -0,0 +1,1013 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "id": "4ec9de62-88a7-4bec-91cc-0a759e84db21",
+ "prevIds": ["572fb732-56f4-4b1e-b981-77152c9980dd"],
+ "ddl": [
+ {
+ "name": "workspace",
+ "entityType": "tables"
+ },
+ {
+ "name": "control_account",
+ "entityType": "tables"
+ },
+ {
+ "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": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "branch",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "directory",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "extra",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "project_id",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "url",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "access_token",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refresh_token",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token_expiry",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "active",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "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": "commands",
+ "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": "workspace_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": ["project_id"],
+ "tableTo": "project",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_workspace_project_id_project_id_fk",
+ "entityType": "fks",
+ "table": "workspace"
+ },
+ {
+ "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": ["email", "url"],
+ "nameExplicit": false,
+ "name": "control_account_pk",
+ "entityType": "pks",
+ "table": "control_account"
+ },
+ {
+ "columns": ["session_id", "position"],
+ "nameExplicit": false,
+ "name": "todo_pk",
+ "entityType": "pks",
+ "table": "todo"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "workspace_pk",
+ "table": "workspace",
+ "entityType": "pks"
+ },
+ {
+ "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": "workspace_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_workspace_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": []
+}
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 45ecafaa2..1afb22b6d 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.2.15",
+ "version": "1.2.17",
"name": "opencode",
"type": "module",
"license": "MIT",
diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts
index 776cc99b4..80253a665 100644
--- a/packages/opencode/src/auth/index.ts
+++ b/packages/opencode/src/auth/index.ts
@@ -56,13 +56,18 @@ export namespace Auth {
}
export async function set(key: string, info: Info) {
+ const normalized = key.replace(/\/+$/, "")
const data = await all()
- await Filesystem.writeJson(filepath, { ...data, [key]: info }, 0o600)
+ if (normalized !== key) delete data[key]
+ delete data[normalized + "/"]
+ await Filesystem.writeJson(filepath, { ...data, [normalized]: info }, 0o600)
}
export async function remove(key: string) {
+ const normalized = key.replace(/\/+$/, "")
const data = await all()
delete data[key]
+ delete data[normalized]
await Filesystem.writeJson(filepath, data, 0o600)
}
}
diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts
index 956359164..4afe7a822 100644
--- a/packages/opencode/src/cli/cmd/auth.ts
+++ b/packages/opencode/src/cli/cmd/auth.ts
@@ -263,7 +263,8 @@ export const AuthLoginCommand = cmd({
UI.empty()
prompts.intro("Add credential")
if (args.url) {
- const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any)
+ const url = args.url.replace(/\/+$/, "")
+ const wellknown = await fetch(`${url}/.well-known/opencode`).then((x) => x.json() as any)
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
const proc = Process.spawn(wellknown.auth.command, {
stdout: "pipe",
@@ -279,12 +280,12 @@ export const AuthLoginCommand = cmd({
prompts.outro("Done")
return
}
- await Auth.set(args.url, {
+ await Auth.set(url, {
type: "wellknown",
key: wellknown.auth.env,
token: token.trim(),
})
- prompts.log.success("Logged into " + args.url)
+ prompts.log.success("Logged into " + url)
prompts.outro("Done")
return
}
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index f3781f1ab..61bc609bb 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -555,6 +555,45 @@ export const RunCommand = cmd({
// Validate agent if specified
const agent = await (async () => {
if (!args.agent) return undefined
+
+ // When attaching, validate against the running server instead of local Instance state.
+ if (args.attach) {
+ const modes = await sdk.app
+ .agents(undefined, { throwOnError: true })
+ .then((x) => x.data ?? [])
+ .catch(() => undefined)
+
+ if (!modes) {
+ UI.println(
+ UI.Style.TEXT_WARNING_BOLD + "!",
+ UI.Style.TEXT_NORMAL,
+ `failed to list agents from ${args.attach}. Falling back to default agent`,
+ )
+ return undefined
+ }
+
+ const agent = modes.find((a) => a.name === args.agent)
+ if (!agent) {
+ UI.println(
+ UI.Style.TEXT_WARNING_BOLD + "!",
+ UI.Style.TEXT_NORMAL,
+ `agent "${args.agent}" not found. Falling back to default agent`,
+ )
+ return undefined
+ }
+
+ if (agent.mode === "subagent") {
+ UI.println(
+ UI.Style.TEXT_WARNING_BOLD + "!",
+ UI.Style.TEXT_NORMAL,
+ `agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`,
+ )
+ return undefined
+ }
+
+ return args.agent
+ }
+
const entry = await Agent.get(args.agent)
if (!entry) {
UI.println(
diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts
index 8f4bb0144..ab51fe8c3 100644
--- a/packages/opencode/src/cli/cmd/serve.ts
+++ b/packages/opencode/src/cli/cmd/serve.ts
@@ -18,14 +18,7 @@ export const ServeCommand = cmd({
const server = Server.listen(opts)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
- let workspaceSync: Array<ReturnType<typeof Workspace.startSyncing>> = []
- // Only available in development right now
- if (Installation.isLocal()) {
- workspaceSync = Project.list().map((project) => Workspace.startSyncing(project))
- }
-
await new Promise(() => {})
await server.stop()
- await Promise.all(workspaceSync.map((item) => item.stop()))
},
})
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 9d7098413..40e8fbbed 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -154,7 +154,7 @@ export function Session() {
const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
- const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
+ const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", true)
const [showHeader, setShowHeader] = kv.signal("header_visible", true)
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
@@ -241,7 +241,6 @@ export function Session() {
const logo = UI.logo(" ").split(/\r?\n/)
return exit.message.set(
[
- ``,
`${logo[0] ?? ""}`,
`${logo[1] ?? ""}`,
`${logo[2] ?? ""}`,
@@ -1412,13 +1411,6 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
// OpenRouter sends encrypted reasoning data that appears as [REDACTED]
return props.part.text.replace("[REDACTED]", "").trim()
})
- const streaming = createMemo(() => {
- if (!props.last) return false
- if (props.part.time.end) return false
- if (props.message.time.completed) return false
- if (props.message.error) return false
- return true
- })
return (
<Show when={content() && ctx.showThinking()}>
<box
@@ -1433,7 +1425,7 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
<code
filetype="markdown"
drawUnstyledText={false}
- streaming={streaming()}
+ streaming={true}
syntaxStyle={subtleSyntax()}
content={"_Thinking:_ " + content()}
conceal={ctx.conceal()}
@@ -1447,13 +1439,6 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) {
const ctx = use()
const { theme, syntax } = useTheme()
- const streaming = createMemo(() => {
- if (!props.last) return false
- if (props.part.time?.end) return false
- if (props.message.time.completed) return false
- if (props.message.error) return false
- return true
- })
return (
<Show when={props.part.text.trim()}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
@@ -1461,20 +1446,16 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
<Match when={Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<markdown
syntaxStyle={syntax()}
- streaming={streaming()}
+ streaming={true}
content={props.part.text.trim()}
conceal={ctx.conceal()}
- tableOptions={{
- widthMode: "full",
- columnFitter: "balanced",
- }}
/>
</Match>
<Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<code
filetype="markdown"
drawUnstyledText={false}
- streaming={streaming()}
+ streaming={true}
syntaxStyle={syntax()}
content={props.part.text.trim()}
conceal={ctx.conceal()}
@@ -1897,10 +1878,8 @@ function Read(props: ToolProps<typeof ReadTool>) {
</InlineTool>
<For each={loaded()}>
{(filepath) => (
- <box paddingLeft={3}>
- <text paddingLeft={3} fg={theme.textMuted}>
- ↳ Loaded {normalizePath(filepath)}
- </text>
+ <box paddingLeft={5}>
+ <text fg={theme.textMuted}>⤷ Loaded {normalizePath(filepath)}</text>
</box>
)}
</For>
@@ -1994,33 +1973,32 @@ function Task(props: ToolProps<typeof TaskTool>) {
return assistant - first
})
+ const content = createMemo(() => {
+ if (!props.input.description) return ""
+ let content = [`Task ${props.input.description}`]
+
+ if (isRunning() && tools().length > 0) {
+ // content[0] += ` · ${tools().length} toolcalls`
+ if (current()) content.push(`↳ ${Locale.titlecase(current()!.tool)} ${(current()!.state as any).title}`)
+ else content.push(`↳ ${tools().length} toolcalls`)
+ }
+
+ if (props.part.state.status === "completed") {
+ content.push(`└ ${tools().length} toolcalls · ${Locale.duration(duration())}`)
+ }
+
+ return content.join("\n")
+ })
+
return (
<InlineTool
- icon="≡"
+ icon="│"
spinner={isRunning()}
complete={props.input.description}
pending="Delegating..."
part={props.part}
>
- {props.input.description}
- <Show when={isRunning() && tools().length > 0}>
- {" "}
- · {tools().length} toolcalls
- <Show fallback={"\n└ Running..."} when={current()}>
- {(item) => {
- const title = createMemo(() => (item().state as any).title)
- return (
- <>
- {"\n"}└ {Locale.titlecase(item().tool)} {title()}
- </>
- )
- }}
- </Show>
- </Show>
- <Show when={duration() && props.part.state.status === "completed"}>
- {"\n "}
- {tools().length} toolcalls · {Locale.duration(duration())}
- </Show>
+ {content()}
</InlineTool>
)
}
@@ -2240,10 +2218,16 @@ function Diagnostics(props: { diagnostics?: Record<string, Record<string, any>[]
function normalizePath(input?: string) {
if (!input) return ""
- if (path.isAbsolute(input)) {
- return path.relative(process.cwd(), input) || "."
- }
- return input
+
+ const cwd = process.cwd()
+ const absolute = path.isAbsolute(input) ? input : path.resolve(cwd, input)
+ const relative = path.relative(cwd, absolute)
+
+ if (!relative) return "."
+ if (!relative.startsWith("..")) return relative
+
+ // outside cwd - use absolute
+ return absolute
}
function input(input: Record<string, any>, omit?: string[]): string {
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index 750347d9d..f53cc3925 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/packages/opencode/src/cli/cmd/tui/thread.ts
@@ -5,8 +5,8 @@ import { type rpc } from "./worker"
import path from "path"
import { fileURLToPath } from "url"
import { UI } from "@/cli/ui"
-import { iife } from "@/util/iife"
import { Log } from "@/util/log"
+import { withTimeout } from "@/util/timeout"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import { Filesystem } from "@/util/filesystem"
import type { Event } from "@opencode-ai/sdk/v2"
@@ -45,6 +45,20 @@ function createEventSource(client: RpcClient): EventSource {
}
}
+async function target() {
+ if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
+ const dist = new URL("./cli/cmd/tui/worker.js", import.meta.url)
+ if (await Filesystem.exists(fileURLToPath(dist))) return dist
+ return new URL("./worker.ts", import.meta.url)
+}
+
+async function input(value?: string) {
+ const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text()
+ if (!value) return piped
+ if (!piped) return value
+ return piped + "\n" + value
+}
+
export const TuiThreadCommand = cmd({
command: "$0 [project]",
describe: "start opencode tui",
@@ -97,23 +111,17 @@ export const TuiThreadCommand = cmd({
}
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
- const baseCwd = process.env.PWD ?? process.cwd()
- const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
- const localWorker = new URL("./worker.ts", import.meta.url)
- const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
- const workerPath = await iife(async () => {
- if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
- if (await Filesystem.exists(fileURLToPath(distWorker))) return distWorker
- return localWorker
- })
+ const root = process.env.PWD ?? process.cwd()
+ const cwd = args.project ? path.resolve(root, args.project) : process.cwd()
+ const file = await target()
try {
process.chdir(cwd)
- } catch (e) {
+ } catch {
UI.error("Failed to change directory to " + cwd)
return
}
- const worker = new Worker(workerPath, {
+ const worker = new Worker(file, {
env: Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
),
@@ -121,76 +129,88 @@ export const TuiThreadCommand = cmd({
worker.onerror = (e) => {
Log.Default.error(e)
}
+
const client = Rpc.client<typeof rpc>(worker)
- process.on("uncaughtException", (e) => {
- Log.Default.error(e)
- })
- process.on("unhandledRejection", (e) => {
+ const error = (e: unknown) => {
Log.Default.error(e)
- })
- process.on("SIGUSR2", async () => {
- await client.call("reload", undefined)
- })
+ }
+ const reload = () => {
+ client.call("reload", undefined).catch((err) => {
+ Log.Default.warn("worker reload failed", {
+ error: err instanceof Error ? err.message : String(err),
+ })
+ })
+ }
+ process.on("uncaughtException", error)
+ process.on("unhandledRejection", error)
+ process.on("SIGUSR2", reload)
- const prompt = await iife(async () => {
- const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
- if (!args.prompt) return piped
- return piped ? piped + "\n" + args.prompt : args.prompt
- })
+ let stopped = false
+ const stop = async () => {
+ if (stopped) return
+ stopped = true
+ process.off("uncaughtException", error)
+ process.off("unhandledRejection", error)
+ process.off("SIGUSR2", reload)
+ await withTimeout(client.call("shutdown", undefined), 5000).catch((error) => {
+ Log.Default.warn("worker shutdown failed", {
+ error: error instanceof Error ? error.message : String(error),
+ })
+ })
+ worker.terminate()
+ }
+
+ const prompt = await input(args.prompt)
const config = await Instance.provide({
directory: cwd,
fn: () => TuiConfig.get(),
})
- // Check if server should be started (port or hostname explicitly set in CLI or config)
- const networkOpts = await resolveNetworkOptions(args)
- const shouldStartServer =
+ const network = await resolveNetworkOptions(args)
+ const external =
process.argv.includes("--port") ||
process.argv.includes("--hostname") ||
process.argv.includes("--mdns") ||
- networkOpts.mdns ||
- networkOpts.port !== 0 ||
- networkOpts.hostname !== "127.0.0.1"
-
- let url: string
- let customFetch: typeof fetch | undefined
- let events: EventSource | undefined
-
- if (shouldStartServer) {
- // Start HTTP server for external access
- const server = await client.call("server", networkOpts)
- url = server.url
- } else {
- // Use direct RPC communication (no HTTP)
- url = "http://opencode.internal"
- customFetch = createWorkerFetch(client)
- events = createEventSource(client)
- }
+ network.mdns ||
+ network.port !== 0 ||
+ network.hostname !== "127.0.0.1"
- const tuiPromise = tui({
- url,
- config,
- directory: cwd,
- fetch: customFetch,
- events,
- args: {
- continue: args.continue,
- sessionID: args.session,
- agent: args.agent,
- model: args.model,
- prompt,
- fork: args.fork,
- },
- onExit: async () => {
- await client.call("shutdown", undefined)
- },
- })
+ const transport = external
+ ? {
+ url: (await client.call("server", network)).url,
+ fetch: undefined,
+ events: undefined,
+ }
+ : {
+ url: "http://opencode.internal",
+ fetch: createWorkerFetch(client),
+ events: createEventSource(client),
+ }
setTimeout(() => {
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
- }, 1000)
+ }, 1000).unref?.()
- await tuiPromise
+ try {
+ await tui({
+ url: transport.url,
+ config,
+ directory: cwd,
+ fetch: transport.fetch,
+ events: transport.events,
+ args: {
+ continue: args.continue,
+ sessionID: args.session,
+ agent: args.agent,
+ model: args.model,
+ prompt,
+ fork: args.fork,
+ },
+ onExit: stop,
+ })
+ } finally {
+ await stop()
+ }
} finally {
unguard?.()
}
diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts
index bb5495c48..e63f10ba8 100644
--- a/packages/opencode/src/cli/cmd/tui/worker.ts
+++ b/packages/opencode/src/cli/cmd/tui/worker.ts
@@ -137,12 +137,7 @@ export const rpc = {
async shutdown() {
Log.Default.info("worker shutting down")
if (eventStream.abort) eventStream.abort.abort()
- await Promise.race([
- Instance.disposeAll(),
- new Promise((resolve) => {
- setTimeout(resolve, 5000)
- }),
- ])
+ await Instance.disposeAll()
if (server) server.stop(true)
},
}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 141f61569..28c5b239a 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -86,11 +86,12 @@ export namespace Config {
let result: Info = {}
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
+ const url = key.replace(/\/+$/, "")
process.env[value.key] = value.token
- log.debug("fetching remote config", { url: `${key}/.well-known/opencode` })
- const response = await fetch(`${key}/.well-known/opencode`)
+ log.debug("fetching remote config", { url: `${url}/.well-known/opencode` })
+ const response = await fetch(`${url}/.well-known/opencode`)
if (!response.ok) {
- throw new Error(`failed to fetch remote config from ${key}: ${response.status}`)
+ throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
}
const wellknown = (await response.json()) as any
const remoteConfig = wellknown.config ?? {}
@@ -99,11 +100,11 @@ export namespace Config {
result = mergeConfigConcatArrays(
result,
await load(JSON.stringify(remoteConfig), {
- dir: path.dirname(`${key}/.well-known/opencode`),
- source: `${key}/.well-known/opencode`,
+ dir: path.dirname(`${url}/.well-known/opencode`),
+ source: `${url}/.well-known/opencode`,
}),
)
- log.debug("loaded remote config from well-known", { url: key })
+ log.debug("loaded remote config from well-known", { url })
}
}
diff --git a/packages/opencode/src/control-plane/adaptors/index.ts b/packages/opencode/src/control-plane/adaptors/index.ts
index 77e1f53c6..a43fce248 100644
--- a/packages/opencode/src/control-plane/adaptors/index.ts
+++ b/packages/opencode/src/control-plane/adaptors/index.ts
@@ -1,10 +1,20 @@
-import { WorktreeAdaptor } from "./worktree"
-import type { Config } from "../config"
-import type { Adaptor } from "./types"
-
-export function getAdaptor(config: Config): Adaptor {
- switch (config.type) {
- case "worktree":
- return WorktreeAdaptor
- }
+import { lazy } from "@/util/lazy"
+import type { Adaptor } from "../types"
+
+const ADAPTORS: Record<string, () => Promise<Adaptor>> = {
+ worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),
+}
+
+export function getAdaptor(type: string): Promise<Adaptor> {
+ return ADAPTORS[type]()
+}
+
+export function installAdaptor(type: string, adaptor: Adaptor) {
+ // This is experimental: mostly used for testing right now, but we
+ // will likely allow this in the future. Need to figure out the
+ // TypeScript story
+
+ // @ts-expect-error we force the builtin types right now, but we
+ // will implement a way to extend the types for custom adaptors
+ ADAPTORS[type] = () => adaptor
}
diff --git a/packages/opencode/src/control-plane/adaptors/types.ts b/packages/opencode/src/control-plane/adaptors/types.ts
deleted file mode 100644
index 47a0405a5..000000000
--- a/packages/opencode/src/control-plane/adaptors/types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { Config } from "../config"
-
-export type Adaptor<T extends Config = Config> = {
- create(from: T, branch?: string | null): Promise<{ config: T; init: () => Promise<void> }>
- remove(from: T): Promise<void>
- request(from: T, method: string, url: string, data?: BodyInit, signal?: AbortSignal): Promise<Response | undefined>
-}
diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts
index e355bb770..f84890950 100644
--- a/packages/opencode/src/control-plane/adaptors/worktree.ts
+++ b/packages/opencode/src/control-plane/adaptors/worktree.ts
@@ -1,26 +1,46 @@
+import z from "zod"
import { Worktree } from "@/worktree"
-import type { Config } from "../config"
-import type { Adaptor } from "./types"
+import { type Adaptor, WorkspaceInfo } from "../types"
-type WorktreeConfig = Extract<Config, { type: "worktree" }>
+const Config = WorkspaceInfo.extend({
+ name: WorkspaceInfo.shape.name.unwrap(),
+ branch: WorkspaceInfo.shape.branch.unwrap(),
+ directory: WorkspaceInfo.shape.directory.unwrap(),
+})
-export const WorktreeAdaptor: Adaptor<WorktreeConfig> = {
- async create(_from: WorktreeConfig, _branch: string) {
- const next = await Worktree.create(undefined)
+type Config = z.infer<typeof Config>
+
+export const WorktreeAdaptor: Adaptor = {
+ async configure(info) {
+ const worktree = await Worktree.makeWorktreeInfo(info.name ?? undefined)
return {
- config: {
- type: "worktree",
- directory: next.directory,
- },
- // Hack for now: `Worktree.create` puts all its async code in a
- // `setTimeout` so it doesn't use this, but we should change that
- init: async () => {},
+ ...info,
+ name: worktree.name,
+ branch: worktree.branch,
+ directory: worktree.directory,
}
},
- async remove(config: WorktreeConfig) {
+ async create(info) {
+ const config = Config.parse(info)
+ const bootstrap = await Worktree.createFromInfo({
+ name: config.name,
+ directory: config.directory,
+ branch: config.branch,
+ })
+ return bootstrap()
+ },
+ async remove(info) {
+ const config = Config.parse(info)
await Worktree.remove({ directory: config.directory })
},
- async request(_from: WorktreeConfig, _method: string, _url: string, _data?: BodyInit, _signal?: AbortSignal) {
- throw new Error("worktree does not support request")
+ async fetch(info, input: RequestInfo | URL, init?: RequestInit) {
+ const config = Config.parse(info)
+ const { WorkspaceServer } = await import("../workspace-server/server")
+ const url = input instanceof Request || input instanceof URL ? input : new URL(input, "http://opencode.internal")
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined))
+ headers.set("x-opencode-directory", config.directory)
+
+ const request = new Request(url, { ...init, headers })
+ return WorkspaceServer.App().fetch(request)
},
}
diff --git a/packages/opencode/src/control-plane/config.ts b/packages/opencode/src/control-plane/config.ts
deleted file mode 100644
index 73dbc4bdb..000000000
--- a/packages/opencode/src/control-plane/config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import z from "zod"
-
-export const Config = z.discriminatedUnion("type", [
- z.object({
- directory: z.string(),
- type: z.literal("worktree"),
- }),
-])
-
-export type Config = z.infer<typeof Config>
diff --git a/packages/opencode/src/control-plane/session-proxy-middleware.ts b/packages/opencode/src/control-plane/session-proxy-middleware.ts
deleted file mode 100644
index df2591017..000000000
--- a/packages/opencode/src/control-plane/session-proxy-middleware.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Instance } from "@/project/instance"
-import type { MiddlewareHandler } from "hono"
-import { Installation } from "../installation"
-import { getAdaptor } from "./adaptors"
-import { Workspace } from "./workspace"
-
-// This middleware forwards all non-GET requests if the workspace is a
-// remote. The remote workspace needs to handle session mutations
-async function proxySessionRequest(req: Request) {
- if (req.method === "GET") return
- if (!Instance.directory.startsWith("wrk_")) return
-
- const workspace = await Workspace.get(Instance.directory)
- if (!workspace) {
- return new Response(`Workspace not found: ${Instance.directory}`, {
- status: 500,
- headers: {
- "content-type": "text/plain; charset=utf-8",
- },
- })
- }
- if (workspace.config.type === "worktree") return
-
- const url = new URL(req.url)
- const body = req.method === "HEAD" ? undefined : await req.arrayBuffer()
- return getAdaptor(workspace.config).request(
- workspace.config,
- req.method,
- `${url.pathname}${url.search}`,
- body,
- req.signal,
- )
-}
-
-export const SessionProxyMiddleware: MiddlewareHandler = async (c, next) => {
- // Only available in development for now
- if (!Installation.isLocal()) {
- return next()
- }
-
- const response = await proxySessionRequest(c.req.raw)
- if (response) {
- return response
- }
- return next()
-}
diff --git a/packages/opencode/src/control-plane/types.ts b/packages/opencode/src/control-plane/types.ts
new file mode 100644
index 000000000..3d27757fd
--- /dev/null
+++ b/packages/opencode/src/control-plane/types.ts
@@ -0,0 +1,20 @@
+import z from "zod"
+import { Identifier } from "@/id/id"
+
+export const WorkspaceInfo = z.object({
+ id: Identifier.schema("workspace"),
+ type: z.string(),
+ branch: z.string().nullable(),
+ name: z.string().nullable(),
+ directory: z.string().nullable(),
+ extra: z.unknown().nullable(),
+ projectID: z.string(),
+})
+export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
+
+export type Adaptor = {
+ configure(input: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
+ create(input: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
+ remove(config: WorkspaceInfo): Promise<void>
+ fetch(config: WorkspaceInfo, input: RequestInfo | URL, init?: RequestInit): Promise<Response>
+}
diff --git a/packages/opencode/src/control-plane/workspace-router-middleware.ts b/packages/opencode/src/control-plane/workspace-router-middleware.ts
new file mode 100644
index 000000000..b48f2fd2b
--- /dev/null
+++ b/packages/opencode/src/control-plane/workspace-router-middleware.ts
@@ -0,0 +1,50 @@
+import { Instance } from "@/project/instance"
+import type { MiddlewareHandler } from "hono"
+import { Installation } from "../installation"
+import { getAdaptor } from "./adaptors"
+import { Workspace } from "./workspace"
+import { WorkspaceContext } from "./workspace-context"
+
+// This middleware forwards all non-GET requests if the workspace is a
+// remote. The remote workspace needs to handle session mutations
+async function routeRequest(req: Request) {
+ // Right now, we need to forward all requests to the workspace
+ // because we don't have syncing. In the future all GET requests
+ // which don't mutate anything will be handled locally
+ //
+ // if (req.method === "GET") return
+
+ if (!WorkspaceContext.workspaceID) return
+
+ const workspace = await Workspace.get(WorkspaceContext.workspaceID)
+ if (!workspace) {
+ return new Response(`Workspace not found: ${WorkspaceContext.workspaceID}`, {
+ status: 500,
+ headers: {
+ "content-type": "text/plain; charset=utf-8",
+ },
+ })
+ }
+
+ const adaptor = await getAdaptor(workspace.type)
+
+ return adaptor.fetch(workspace, `${new URL(req.url).pathname}${new URL(req.url).search}`, {
+ method: req.method,
+ body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
+ signal: req.signal,
+ headers: req.headers,
+ })
+}
+
+export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c, next) => {
+ // Only available in development for now
+ if (!Installation.isLocal()) {
+ return next()
+ }
+
+ const response = await routeRequest(c.req.raw)
+ if (response) {
+ return response
+ }
+ return next()
+}
diff --git a/packages/opencode/src/control-plane/workspace-server/server.ts b/packages/opencode/src/control-plane/workspace-server/server.ts
index 716989942..fd7fd9308 100644
--- a/packages/opencode/src/control-plane/workspace-server/server.ts
+++ b/packages/opencode/src/control-plane/workspace-server/server.ts
@@ -1,17 +1,57 @@
import { Hono } from "hono"
+import { Instance } from "../../project/instance"
+import { InstanceBootstrap } from "../../project/bootstrap"
import { SessionRoutes } from "../../server/routes/session"
import { WorkspaceServerRoutes } from "./routes"
+import { WorkspaceContext } from "../workspace-context"
export namespace WorkspaceServer {
export function App() {
const session = new Hono()
- .use("*", async (c, next) => {
- if (c.req.method === "GET") return c.notFound()
+ .use(async (c, next) => {
+ // Right now, we need handle all requests because we don't
+ // have syncing. In the future all GET requests will handled
+ // by the control plane
+ //
+ // if (c.req.method === "GET") return c.notFound()
await next()
})
.route("/", SessionRoutes())
- return new Hono().route("/session", session).route("/", WorkspaceServerRoutes())
+ return new Hono()
+ .use(async (c, next) => {
+ const workspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
+ const raw = c.req.query("directory") || c.req.header("x-opencode-directory")
+ if (workspaceID == null) {
+ throw new Error("workspaceID parameter is required")
+ }
+ if (raw == null) {
+ throw new Error("directory parameter is required")
+ }
+
+ const directory = (() => {
+ try {
+ return decodeURIComponent(raw)
+ } catch {
+ return raw
+ }
+ })()
+
+ return WorkspaceContext.provide({
+ workspaceID,
+ async fn() {
+ return Instance.provide({
+ directory,
+ init: InstanceBootstrap,
+ async fn() {
+ return next()
+ },
+ })
+ },
+ })
+ })
+ .route("/session", session)
+ .route("/", WorkspaceServerRoutes())
}
export function Listen(opts: { hostname: string; port: number }) {
diff --git a/packages/opencode/src/control-plane/workspace.sql.ts b/packages/opencode/src/control-plane/workspace.sql.ts
index 1a2011982..1ba1605f8 100644
--- a/packages/opencode/src/control-plane/workspace.sql.ts
+++ b/packages/opencode/src/control-plane/workspace.sql.ts
@@ -1,12 +1,14 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "@/project/project.sql"
-import type { Config } from "./config"
export const WorkspaceTable = sqliteTable("workspace", {
id: text().primaryKey(),
+ type: text().notNull(),
branch: text(),
+ name: text(),
+ directory: text(),
+ extra: text({ mode: "json" }),
project_id: text()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
- config: text({ mode: "json" }).notNull().$type<Config>(),
})
diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts
index 5ce373b12..8c76fbdab 100644
--- a/packages/opencode/src/control-plane/workspace.ts
+++ b/packages/opencode/src/control-plane/workspace.ts
@@ -7,8 +7,8 @@ import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global"
import { Log } from "@/util/log"
import { WorkspaceTable } from "./workspace.sql"
-import { Config } from "./config"
import { getAdaptor } from "./adaptors"
+import { WorkspaceInfo } from "./types"
import { parseSSE } from "./sse"
export namespace Workspace {
@@ -27,72 +27,64 @@ export namespace Workspace {
),
}
- export const Info = z
- .object({
- id: Identifier.schema("workspace"),
- branch: z.string().nullable(),
- projectID: z.string(),
- config: Config,
- })
- .meta({
- ref: "Workspace",
- })
+ export const Info = WorkspaceInfo.meta({
+ ref: "Workspace",
+ })
export type Info = z.infer<typeof Info>
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
return {
id: row.id,
+ type: row.type,
branch: row.branch,
+ name: row.name,
+ directory: row.directory,
+ extra: row.extra,
projectID: row.project_id,
- config: row.config,
}
}
- export const create = fn(
- z.object({
- id: Identifier.schema("workspace").optional(),
- projectID: Info.shape.projectID,
- branch: Info.shape.branch,
- config: Info.shape.config,
- }),
- async (input) => {
- const id = Identifier.ascending("workspace", input.id)
-
- const { config, init } = await getAdaptor(input.config).create(input.config, input.branch)
-
- const info: Info = {
- id,
- projectID: input.projectID,
- branch: input.branch,
- config,
- }
+ const CreateInput = z.object({
+ id: Identifier.schema("workspace").optional(),
+ type: Info.shape.type,
+ branch: Info.shape.branch,
+ projectID: Info.shape.projectID,
+ extra: Info.shape.extra,
+ })
- setTimeout(async () => {
- await init()
-
- Database.use((db) => {
- db.insert(WorkspaceTable)
- .values({
- id: info.id,
- branch: info.branch,
- project_id: info.projectID,
- config: info.config,
- })
- .run()
- })
+ export const create = fn(CreateInput, async (input) => {
+ const id = Identifier.ascending("workspace", input.id)
+ const adaptor = await getAdaptor(input.type)
- GlobalBus.emit("event", {
- directory: id,
- payload: {
- type: Event.Ready.type,
- properties: {},
- },
+ const config = await adaptor.configure({ ...input, id, name: null, directory: null })
+
+ const info: Info = {
+ id,
+ type: config.type,
+ branch: config.branch ?? null,
+ name: config.name ?? null,
+ directory: config.directory ?? null,
+ extra: config.extra ?? null,
+ projectID: input.projectID,
+ }
+
+ Database.use((db) => {
+ db.insert(WorkspaceTable)
+ .values({
+ id: info.id,
+ type: info.type,
+ branch: info.branch,
+ name: info.name,
+ directory: info.directory,
+ extra: info.extra,
+ project_id: info.projectID,
})
- }, 0)
+ .run()
+ })
- return info
- },
- )
+ await adaptor.create(config)
+ return info
+ })
export function list(project: Project.Info) {
const rows = Database.use((db) =>
@@ -111,7 +103,8 @@ export namespace Workspace {
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
if (row) {
const info = fromRow(row)
- await getAdaptor(info.config).remove(info.config)
+ const adaptor = await getAdaptor(row.type)
+ adaptor.remove(info)
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
return info
}
@@ -120,9 +113,8 @@ export namespace Workspace {
async function workspaceEventLoop(space: Info, stop: AbortSignal) {
while (!stop.aborted) {
- const res = await getAdaptor(space.config)
- .request(space.config, "GET", "/event", undefined, stop)
- .catch(() => undefined)
+ const adaptor = await getAdaptor(space.type)
+ const res = await adaptor.fetch(space, "/event", { method: "GET", signal: stop }).catch(() => undefined)
if (!res || !res.ok || !res.body) {
await Bun.sleep(1000)
continue
@@ -140,7 +132,7 @@ export namespace Workspace {
export function startSyncing(project: Project.Info) {
const stop = new AbortController()
- const spaces = list(project).filter((space) => space.config.type !== "worktree")
+ const spaces = list(project).filter((space) => space.type !== "worktree")
spaces.forEach((space) => {
void workspaceEventLoop(space, stop.signal).catch((error) => {
diff --git a/packages/opencode/src/server/routes/experimental.ts b/packages/opencode/src/server/routes/experimental.ts
index 892bca485..98c7ece10 100644
--- a/packages/opencode/src/server/routes/experimental.ts
+++ b/packages/opencode/src/server/routes/experimental.ts
@@ -88,6 +88,7 @@ export const ExperimentalRoutes = lazy(() =>
)
},
)
+ .route("/workspace", WorkspaceRoutes())
.post(
"/worktree",
describeRoute({
@@ -113,7 +114,6 @@ export const ExperimentalRoutes = lazy(() =>
return c.json(worktree)
},
)
- .route("/workspace", WorkspaceRoutes())
.get(
"/worktree",
describeRoute({
diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts
index a39197952..12938aeab 100644
--- a/packages/opencode/src/server/routes/session.ts
+++ b/packages/opencode/src/server/routes/session.ts
@@ -16,13 +16,11 @@ import { Log } from "../../util/log"
import { PermissionNext } from "@/permission/next"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
-import { SessionProxyMiddleware } from "../../control-plane/session-proxy-middleware"
const log = Log.create({ service: "server" })
export const SessionRoutes = lazy(() =>
new Hono()
- .use(SessionProxyMiddleware)
.get(
"/",
describeRoute({
diff --git a/packages/opencode/src/server/routes/workspace.ts b/packages/opencode/src/server/routes/workspace.ts
index 0c64c9cd4..cd2d844ae 100644
--- a/packages/opencode/src/server/routes/workspace.ts
+++ b/packages/opencode/src/server/routes/workspace.ts
@@ -9,7 +9,7 @@ import { lazy } from "../../util/lazy"
export const WorkspaceRoutes = lazy(() =>
new Hono()
.post(
- "/:id",
+ "/",
describeRoute({
summary: "Create workspace",
description: "Create a workspace for the current project.",
@@ -27,26 +27,16 @@ export const WorkspaceRoutes = lazy(() =>
},
}),
validator(
- "param",
- z.object({
- id: Workspace.Info.shape.id,
- }),
- ),
- validator(
"json",
- z.object({
- branch: Workspace.Info.shape.branch,
- config: Workspace.Info.shape.config,
+ Workspace.create.schema.omit({
+ projectID: true,
}),
),
async (c) => {
- const { id } = c.req.valid("param")
const body = c.req.valid("json")
const workspace = await Workspace.create({
- id,
projectID: Instance.project.id,
- branch: body.branch,
- config: body.config,
+ ...body,
})
return c.json(workspace)
},
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 85049650c..6ea66be98 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -22,6 +22,7 @@ import { Flag } from "../flag/flag"
import { Command } from "../command"
import { Global } from "../global"
import { WorkspaceContext } from "../control-plane/workspace-context"
+import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware"
import { ProjectRoutes } from "./routes/project"
import { SessionRoutes } from "./routes/session"
import { PtyRoutes } from "./routes/pty"
@@ -218,6 +219,7 @@ export namespace Server {
},
})
})
+ .use(WorkspaceRouterMiddleware)
.get(
"/doc",
openAPIRouteHandler(app, {
diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt
index 47e9378e7..baafb0081 100644
--- a/packages/opencode/src/tool/bash.txt
+++ b/packages/opencode/src/tool/bash.txt
@@ -24,7 +24,7 @@ Usage notes:
- The command argument is required.
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
- - If the output exceeds ${maxLines} lines or ${maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Because of this, you do NOT need to use `head`, `tail`, or other truncation commands to limit output - just run the command directly.
+ - If the output exceeds ${maxLines} lines or ${maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use `head`, `tail`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching.
- Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
- File search: Use Glob (NOT find or ls)
diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts
index d85a0843f..226732249 100644
--- a/packages/opencode/src/worktree/index.ts
+++ b/packages/opencode/src/worktree/index.ts
@@ -331,7 +331,7 @@ export namespace Worktree {
}, 0)
}
- export const create = fn(CreateInput.optional(), async (input) => {
+ export async function makeWorktreeInfo(name?: string): Promise<Info> {
if (Instance.project.vcs !== "git") {
throw new NotGitError({ message: "Worktrees are only supported for git projects" })
}
@@ -339,9 +339,11 @@ export namespace Worktree {
const root = path.join(Global.Path.data, "worktree", Instance.project.id)
await fs.mkdir(root, { recursive: true })
- const base = input?.name ? slug(input.name) : ""
- const info = await candidate(root, base || undefined)
+ const base = name ? slug(name) : ""
+ return candidate(root, base || undefined)
+ }
+ export async function createFromInfo(info: Info, startCommand?: string) {
const created = await $`git worktree add --no-checkout -b ${info.branch} ${info.directory}`
.quiet()
.nothrow()
@@ -353,8 +355,9 @@ export namespace Worktree {
await Project.addSandbox(Instance.project.id, info.directory).catch(() => undefined)
const projectID = Instance.project.id
- const extra = input?.startCommand?.trim()
- setTimeout(() => {
+ const extra = startCommand?.trim()
+
+ return () => {
const start = async () => {
const populated = await $`git reset --hard`.quiet().nothrow().cwd(info.directory)
if (populated.exitCode !== 0) {
@@ -411,8 +414,17 @@ export namespace Worktree {
void start().catch((error) => {
log.error("worktree start task failed", { directory: info.directory, error })
})
- }, 0)
+ }
+ }
+ export const create = fn(CreateInput.optional(), async (input) => {
+ const info = await makeWorktreeInfo(input?.name)
+ const bootstrap = await createFromInfo(info, input?.startCommand)
+ // This is needed due to how worktrees currently work in the
+ // desktop app
+ setTimeout(() => {
+ bootstrap()
+ }, 0)
return info
})
diff --git a/packages/opencode/test/auth/auth.test.ts b/packages/opencode/test/auth/auth.test.ts
new file mode 100644
index 000000000..a569c7113
--- /dev/null
+++ b/packages/opencode/test/auth/auth.test.ts
@@ -0,0 +1,58 @@
+import { test, expect } from "bun:test"
+import { Auth } from "../../src/auth"
+
+test("set normalizes trailing slashes in keys", async () => {
+ await Auth.set("https://example.com/", {
+ type: "wellknown",
+ key: "TOKEN",
+ token: "abc",
+ })
+ const data = await Auth.all()
+ expect(data["https://example.com"]).toBeDefined()
+ expect(data["https://example.com/"]).toBeUndefined()
+})
+
+test("set cleans up pre-existing trailing-slash entry", async () => {
+ // Simulate a pre-fix entry with trailing slash
+ await Auth.set("https://example.com/", {
+ type: "wellknown",
+ key: "TOKEN",
+ token: "old",
+ })
+ // Re-login with normalized key (as the CLI does post-fix)
+ await Auth.set("https://example.com", {
+ type: "wellknown",
+ key: "TOKEN",
+ token: "new",
+ })
+ const data = await Auth.all()
+ const keys = Object.keys(data).filter((k) => k.includes("example.com"))
+ expect(keys).toEqual(["https://example.com"])
+ const entry = data["https://example.com"]!
+ expect(entry.type).toBe("wellknown")
+ if (entry.type === "wellknown") expect(entry.token).toBe("new")
+})
+
+test("remove deletes both trailing-slash and normalized keys", async () => {
+ await Auth.set("https://example.com", {
+ type: "wellknown",
+ key: "TOKEN",
+ token: "abc",
+ })
+ await Auth.remove("https://example.com/")
+ const data = await Auth.all()
+ expect(data["https://example.com"]).toBeUndefined()
+ expect(data["https://example.com/"]).toBeUndefined()
+})
+
+test("set and remove are no-ops on keys without trailing slashes", async () => {
+ await Auth.set("anthropic", {
+ type: "api",
+ key: "sk-test",
+ })
+ const data = await Auth.all()
+ expect(data["anthropic"]).toBeDefined()
+ await Auth.remove("anthropic")
+ const after = await Auth.all()
+ expect(after["anthropic"]).toBeUndefined()
+})
diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts
index f245dc349..40ab97449 100644
--- a/packages/opencode/test/config/config.test.ts
+++ b/packages/opencode/test/config/config.test.ts
@@ -1535,6 +1535,71 @@ test("project config overrides remote well-known config", async () => {
}
})
+test("wellknown URL with trailing slash is normalized", async () => {
+ const originalFetch = globalThis.fetch
+ let fetchedUrl: string | undefined
+ const mockFetch = mock((url: string | URL | Request) => {
+ const urlStr = url.toString()
+ if (urlStr.includes(".well-known/opencode")) {
+ fetchedUrl = urlStr
+ return Promise.resolve(
+ new Response(
+ JSON.stringify({
+ config: {
+ mcp: {
+ slack: {
+ type: "remote",
+ url: "https://slack.example.com/mcp",
+ enabled: true,
+ },
+ },
+ },
+ }),
+ { status: 200 },
+ ),
+ )
+ }
+ return originalFetch(url)
+ })
+ globalThis.fetch = mockFetch as unknown as typeof fetch
+
+ const originalAuthAll = Auth.all
+ Auth.all = mock(() =>
+ Promise.resolve({
+ "https://example.com/": {
+ type: "wellknown" as const,
+ key: "TEST_TOKEN",
+ token: "test-token",
+ },
+ }),
+ )
+
+ try {
+ await using tmp = await tmpdir({
+ git: true,
+ init: async (dir) => {
+ await Filesystem.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ await Config.get()
+ // Trailing slash should be stripped — no double slash in the fetch URL
+ expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
+ },
+ })
+ } finally {
+ globalThis.fetch = originalFetch
+ Auth.all = originalAuthAll
+ }
+})
+
describe("getPluginName", () => {
test("extracts name from file:// URL", () => {
expect(Config.getPluginName("file:///path/to/plugin/foo.js")).toBe("foo")
diff --git a/packages/opencode/test/control-plane/session-proxy-middleware.test.ts b/packages/opencode/test/control-plane/session-proxy-middleware.test.ts
index 596e4761e..369b9152a 100644
--- a/packages/opencode/test/control-plane/session-proxy-middleware.test.ts
+++ b/packages/opencode/test/control-plane/session-proxy-middleware.test.ts
@@ -5,8 +5,11 @@ import { tmpdir } from "../fixture/fixture"
import { Project } from "../../src/project/project"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import { Instance } from "../../src/project/instance"
+import { WorkspaceContext } from "../../src/control-plane/workspace-context"
import { Database } from "../../src/storage/db"
import { resetDatabase } from "../fixture/db"
+import * as adaptors from "../../src/control-plane/adaptors"
+import type { Adaptor } from "../../src/control-plane/types"
afterEach(async () => {
mock.restore()
@@ -18,18 +21,35 @@ type State = {
calls: Array<{ method: string; url: string; body?: string }>
}
-const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
+const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert
async function setup(state: State) {
- mock.module("../../src/control-plane/adaptors", () => ({
- getAdaptor: () => ({
- request: async (_config: unknown, method: string, url: string, data?: BodyInit) => {
- const body = data ? await new Response(data).text() : undefined
- state.calls.push({ method, url, body })
- return new Response("proxied", { status: 202 })
- },
- }),
- }))
+ const TestAdaptor: Adaptor = {
+ configure(config) {
+ return config
+ },
+ async create() {
+ throw new Error("not used")
+ },
+ async remove() {},
+
+ async fetch(_config: unknown, input: RequestInfo | URL, init?: RequestInit) {
+ const url =
+ input instanceof Request || input instanceof URL
+ ? input.toString()
+ : new URL(input, "http://workspace.test").toString()
+ const request = new Request(url, init)
+ const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.text()
+ state.calls.push({
+ method: request.method,
+ url: `${new URL(request.url).pathname}${new URL(request.url).search}`,
+ body,
+ })
+ return new Response("proxied", { status: 202 })
+ },
+ }
+
+ adaptors.installAdaptor("testing", TestAdaptor)
await using tmp = await tmpdir({ git: true })
const { project } = await Project.fromDirectory(tmp.path)
@@ -45,20 +65,23 @@ async function setup(state: State) {
id: id1,
branch: "main",
project_id: project.id,
- config: remote,
+ type: remote.type,
+ name: remote.name,
},
{
id: id2,
branch: "main",
project_id: project.id,
- config: { type: "worktree", directory: tmp.path },
+ type: "worktree",
+ directory: tmp.path,
+ name: "local",
},
])
.run(),
)
- const { SessionProxyMiddleware } = await import("../../src/control-plane/session-proxy-middleware")
- const app = new Hono().use(SessionProxyMiddleware)
+ const { WorkspaceRouterMiddleware } = await import("../../src/control-plane/workspace-router-middleware")
+ const app = new Hono().use(WorkspaceRouterMiddleware)
return {
id1,
@@ -66,15 +89,19 @@ async function setup(state: State) {
app,
async request(input: RequestInfo | URL, init?: RequestInit) {
return Instance.provide({
- directory: state.workspace === "first" ? id1 : id2,
- fn: async () => app.request(input, init),
+ directory: tmp.path,
+ fn: async () =>
+ WorkspaceContext.provide({
+ workspaceID: state.workspace === "first" ? id1 : id2,
+ fn: () => app.request(input, init),
+ }),
})
},
}
}
describe("control-plane/session-proxy-middleware", () => {
- test("forwards non-GET session requests for remote workspaces", async () => {
+ test("forwards non-GET session requests for workspaces", async () => {
const state: State = {
workspace: "first",
calls: [],
@@ -102,46 +129,21 @@ describe("control-plane/session-proxy-middleware", () => {
])
})
- test("does not forward GET requests", async () => {
- const state: State = {
- workspace: "first",
- calls: [],
- }
+ // It will behave this way when we have syncing
+ //
+ // test("does not forward GET requests", async () => {
+ // const state: State = {
+ // workspace: "first",
+ // calls: [],
+ // }
- const ctx = await setup(state)
+ // const ctx = await setup(state)
- ctx.app.get("/session/foo", (c) => c.text("local", 200))
- const response = await ctx.request("http://workspace.test/session/foo?x=1")
+ // ctx.app.get("/session/foo", (c) => c.text("local", 200))
+ // const response = await ctx.request("http://workspace.test/session/foo?x=1")
- expect(response.status).toBe(200)
- expect(await response.text()).toBe("local")
- expect(state.calls).toEqual([])
- })
-
- test("does not forward GET or POST requests for worktree workspaces", async () => {
- const state: State = {
- workspace: "second",
- calls: [],
- }
-
- const ctx = await setup(state)
-
- ctx.app.get("/session/foo", (c) => c.text("local-get", 200))
- ctx.app.post("/session/foo", (c) => c.text("local-post", 200))
-
- const getResponse = await ctx.request("http://workspace.test/session/foo?x=1")
- const postResponse = await ctx.request("http://workspace.test/session/foo?x=1", {
- method: "POST",
- body: JSON.stringify({ hello: "world" }),
- headers: {
- "content-type": "application/json",
- },
- })
-
- expect(getResponse.status).toBe(200)
- expect(await getResponse.text()).toBe("local-get")
- expect(postResponse.status).toBe(200)
- expect(await postResponse.text()).toBe("local-post")
- expect(state.calls).toEqual([])
- })
+ // expect(response.status).toBe(200)
+ // expect(await response.text()).toBe("local")
+ // expect(state.calls).toEqual([])
+ // })
})
diff --git a/packages/opencode/test/control-plane/workspace-server-sse.test.ts b/packages/opencode/test/control-plane/workspace-server-sse.test.ts
index 91504af0f..7e7cddb14 100644
--- a/packages/opencode/test/control-plane/workspace-server-sse.test.ts
+++ b/packages/opencode/test/control-plane/workspace-server-sse.test.ts
@@ -4,6 +4,7 @@ import { WorkspaceServer } from "../../src/control-plane/workspace-server/server
import { parseSSE } from "../../src/control-plane/sse"
import { GlobalBus } from "../../src/bus/global"
import { resetDatabase } from "../fixture/db"
+import { tmpdir } from "../fixture/fixture"
afterEach(async () => {
await resetDatabase()
@@ -13,13 +14,17 @@ Log.init({ print: false })
describe("control-plane/workspace-server SSE", () => {
test("streams GlobalBus events and parseSSE reads them", async () => {
+ await using tmp = await tmpdir({ git: true })
const app = WorkspaceServer.App()
const stop = new AbortController()
const seen: unknown[] = []
-
try {
const response = await app.request("/event", {
signal: stop.signal,
+ headers: {
+ "x-opencode-workspace": "wrk_test_workspace",
+ "x-opencode-directory": tmp.path,
+ },
})
expect(response.status).toBe(200)
diff --git a/packages/opencode/test/control-plane/workspace-sync.test.ts b/packages/opencode/test/control-plane/workspace-sync.test.ts
index 2769c8a3b..899118920 100644
--- a/packages/opencode/test/control-plane/workspace-sync.test.ts
+++ b/packages/opencode/test/control-plane/workspace-sync.test.ts
@@ -7,6 +7,8 @@ import { Database } from "../../src/storage/db"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import { GlobalBus } from "../../src/bus/global"
import { resetDatabase } from "../fixture/db"
+import * as adaptors from "../../src/control-plane/adaptors"
+import type { Adaptor } from "../../src/control-plane/types"
afterEach(async () => {
mock.restore()
@@ -15,35 +17,34 @@ afterEach(async () => {
Log.init({ print: false })
-const seen: string[] = []
-const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
+const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert
-mock.module("../../src/control-plane/adaptors", () => ({
- getAdaptor: (config: { type: string }) => {
- seen.push(config.type)
- return {
- async create() {
- throw new Error("not used")
+const TestAdaptor: Adaptor = {
+ configure(config) {
+ return config
+ },
+ async create() {
+ throw new Error("not used")
+ },
+ async remove() {},
+ async fetch(_config: unknown, _input: RequestInfo | URL, _init?: RequestInit) {
+ const body = new ReadableStream<Uint8Array>({
+ start(controller) {
+ const encoder = new TextEncoder()
+ controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
+ controller.close()
},
- async remove() {},
- async request() {
- const body = new ReadableStream<Uint8Array>({
- start(controller) {
- const encoder = new TextEncoder()
- controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
- controller.close()
- },
- })
- return new Response(body, {
- status: 200,
- headers: {
- "content-type": "text/event-stream",
- },
- })
+ })
+ return new Response(body, {
+ status: 200,
+ headers: {
+ "content-type": "text/event-stream",
},
- }
+ })
},
-}))
+}
+
+adaptors.installAdaptor("testing", TestAdaptor)
describe("control-plane/workspace.startSyncing", () => {
test("syncs only remote workspaces and emits remote SSE events", async () => {
@@ -62,13 +63,16 @@ describe("control-plane/workspace.startSyncing", () => {
id: id1,
branch: "main",
project_id: project.id,
- config: remote,
+ type: remote.type,
+ name: remote.name,
},
{
id: id2,
branch: "main",
project_id: project.id,
- config: { type: "worktree", directory: tmp.path },
+ type: "worktree",
+ directory: tmp.path,
+ name: "local",
},
])
.run(),
@@ -91,7 +95,5 @@ describe("control-plane/workspace.startSyncing", () => {
])
await sync.stop()
- expect(seen).toContain("testing")
- expect(seen).not.toContain("worktree")
})
})
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index e476c41e2..467cadd61 100644
--- a/packages/plugin/package.json
+++ b/packages/plugin/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
- "version": "1.2.15",
+ "version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json
index ffbdf2198..e83fa7c86 100644
--- a/packages/sdk/js/package.json
+++ b/packages/sdk/js/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
- "version": "1.2.15",
+ "version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 49ebc8473..1c1b31e46 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -862,17 +862,16 @@ export class Tool extends HeyApiClient {
}
}
-export class Worktree extends HeyApiClient {
+export class Workspace extends HeyApiClient {
/**
- * Remove worktree
+ * List workspaces
*
- * Remove a git worktree and delete its branch.
+ * List all workspaces.
*/
- public remove<ThrowOnError extends boolean = false>(
+ public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- worktreeRemoveInput?: WorktreeRemoveInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -883,32 +882,30 @@ export class Worktree extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "worktreeRemoveInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).delete<WorktreeRemoveResponses, WorktreeRemoveErrors, ThrowOnError>({
- url: "/experimental/worktree",
+ return (options?.client ?? this.client).get<ExperimentalWorkspaceListResponses, unknown, ThrowOnError>({
+ url: "/experimental/workspace",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * List worktrees
+ * Create workspace
*
- * List all sandbox worktrees for the current project.
+ * Create a workspace for the current project.
*/
- public list<ThrowOnError extends boolean = false>(
+ public create<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
+ id?: string
+ type?: string
+ branch?: string | null
+ extra?: unknown | null
},
options?: Options<never, ThrowOnError>,
) {
@@ -919,27 +916,40 @@ export class Worktree extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { in: "body", key: "id" },
+ { in: "body", key: "type" },
+ { in: "body", key: "branch" },
+ { in: "body", key: "extra" },
],
},
],
)
- return (options?.client ?? this.client).get<WorktreeListResponses, unknown, ThrowOnError>({
- url: "/experimental/worktree",
+ return (options?.client ?? this.client).post<
+ ExperimentalWorkspaceCreateResponses,
+ ExperimentalWorkspaceCreateErrors,
+ ThrowOnError
+ >({
+ url: "/experimental/workspace",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
/**
- * Create worktree
+ * Remove workspace
*
- * Create a new git worktree for the current project and run any configured startup scripts.
+ * Remove an existing workspace.
*/
- public create<ThrowOnError extends boolean = false>(
- parameters?: {
+ public remove<ThrowOnError extends boolean = false>(
+ parameters: {
+ id: string
directory?: string
workspace?: string
- worktreeCreateInput?: WorktreeCreateInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -948,35 +958,41 @@ export class Worktree extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "id" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "worktreeCreateInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).post<WorktreeCreateResponses, WorktreeCreateErrors, ThrowOnError>({
- url: "/experimental/worktree",
+ return (options?.client ?? this.client).delete<
+ ExperimentalWorkspaceRemoveResponses,
+ ExperimentalWorkspaceRemoveErrors,
+ ThrowOnError
+ >({
+ url: "/experimental/workspace/{id}",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
+}
+export class Session extends HeyApiClient {
/**
- * Reset worktree
+ * List sessions
*
- * Reset a worktree branch to the primary default branch.
+ * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.
*/
- public reset<ThrowOnError extends boolean = false>(
+ public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- worktreeResetInput?: WorktreeResetInput
+ roots?: boolean
+ start?: number
+ cursor?: number
+ search?: string
+ limit?: number
+ archived?: boolean
},
options?: Options<never, ThrowOnError>,
) {
@@ -987,33 +1003,32 @@ export class Worktree extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "worktreeResetInput", map: "body" },
+ { in: "query", key: "roots" },
+ { in: "query", key: "start" },
+ { in: "query", key: "cursor" },
+ { in: "query", key: "search" },
+ { in: "query", key: "limit" },
+ { in: "query", key: "archived" },
],
},
],
)
- return (options?.client ?? this.client).post<WorktreeResetResponses, WorktreeResetErrors, ThrowOnError>({
- url: "/experimental/worktree/reset",
+ return (options?.client ?? this.client).get<ExperimentalSessionListResponses, unknown, ThrowOnError>({
+ url: "/experimental/session",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
}
-export class Workspace extends HeyApiClient {
+export class Resource extends HeyApiClient {
/**
- * Remove workspace
+ * Get MCP resources
*
- * Remove an existing workspace.
+ * Get all available MCP resources from connected servers. Optionally filter by name.
*/
- public remove<ThrowOnError extends boolean = false>(
- parameters: {
- id: string
+ public list<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
},
@@ -1024,39 +1039,48 @@ export class Workspace extends HeyApiClient {
[
{
args: [
- { in: "path", key: "id" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).delete<
- ExperimentalWorkspaceRemoveResponses,
- ExperimentalWorkspaceRemoveErrors,
- ThrowOnError
- >({
- url: "/experimental/workspace/{id}",
+ return (options?.client ?? this.client).get<ExperimentalResourceListResponses, unknown, ThrowOnError>({
+ url: "/experimental/resource",
...options,
...params,
})
}
+}
+export class Experimental extends HeyApiClient {
+ private _workspace?: Workspace
+ get workspace(): Workspace {
+ return (this._workspace ??= new Workspace({ client: this.client }))
+ }
+
+ private _session?: Session
+ get session(): Session {
+ return (this._session ??= new Session({ client: this.client }))
+ }
+
+ private _resource?: Resource
+ get resource(): Resource {
+ return (this._resource ??= new Resource({ client: this.client }))
+ }
+}
+
+export class Worktree extends HeyApiClient {
/**
- * Create workspace
+ * Remove worktree
*
- * Create a workspace for the current project.
+ * Remove a git worktree and delete its branch.
*/
- public create<ThrowOnError extends boolean = false>(
- parameters: {
- id: string
+ public remove<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
- branch?: string | null
- config?: {
- directory: string
- type: "worktree"
- }
+ worktreeRemoveInput?: WorktreeRemoveInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -1065,21 +1089,15 @@ export class Workspace extends HeyApiClient {
[
{
args: [
- { in: "path", key: "id" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "body", key: "branch" },
- { in: "body", key: "config" },
+ { key: "worktreeRemoveInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).post<
- ExperimentalWorkspaceCreateResponses,
- ExperimentalWorkspaceCreateErrors,
- ThrowOnError
- >({
- url: "/experimental/workspace/{id}",
+ return (options?.client ?? this.client).delete<WorktreeRemoveResponses, WorktreeRemoveErrors, ThrowOnError>({
+ url: "/experimental/worktree",
...options,
...params,
headers: {
@@ -1091,9 +1109,9 @@ export class Workspace extends HeyApiClient {
}
/**
- * List workspaces
+ * List worktrees
*
- * List all workspaces.
+ * List all sandbox worktrees for the current project.
*/
public list<ThrowOnError extends boolean = false>(
parameters?: {
@@ -1113,30 +1131,23 @@ export class Workspace extends HeyApiClient {
},
],
)
- return (options?.client ?? this.client).get<ExperimentalWorkspaceListResponses, unknown, ThrowOnError>({
- url: "/experimental/workspace",
+ return (options?.client ?? this.client).get<WorktreeListResponses, unknown, ThrowOnError>({
+ url: "/experimental/worktree",
...options,
...params,
})
}
-}
-export class Session extends HeyApiClient {
/**
- * List sessions
+ * Create worktree
*
- * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.
+ * Create a new git worktree for the current project and run any configured startup scripts.
*/
- public list<ThrowOnError extends boolean = false>(
+ public create<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- roots?: boolean
- start?: number
- cursor?: number
- search?: string
- limit?: number
- archived?: boolean
+ worktreeCreateInput?: WorktreeCreateInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -1147,34 +1158,33 @@ export class Session extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "query", key: "roots" },
- { in: "query", key: "start" },
- { in: "query", key: "cursor" },
- { in: "query", key: "search" },
- { in: "query", key: "limit" },
- { in: "query", key: "archived" },
+ { key: "worktreeCreateInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).get<ExperimentalSessionListResponses, unknown, ThrowOnError>({
- url: "/experimental/session",
+ return (options?.client ?? this.client).post<WorktreeCreateResponses, WorktreeCreateErrors, ThrowOnError>({
+ url: "/experimental/worktree",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
-}
-export class Resource extends HeyApiClient {
/**
- * Get MCP resources
+ * Reset worktree
*
- * Get all available MCP resources from connected servers. Optionally filter by name.
+ * Reset a worktree branch to the primary default branch.
*/
- public list<ThrowOnError extends boolean = false>(
+ public reset<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
+ worktreeResetInput?: WorktreeResetInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -1185,35 +1195,24 @@ export class Resource extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { key: "worktreeResetInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).get<ExperimentalResourceListResponses, unknown, ThrowOnError>({
- url: "/experimental/resource",
+ return (options?.client ?? this.client).post<WorktreeResetResponses, WorktreeResetErrors, ThrowOnError>({
+ url: "/experimental/worktree/reset",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
}
-export class Experimental extends HeyApiClient {
- private _workspace?: Workspace
- get workspace(): Workspace {
- return (this._workspace ??= new Workspace({ client: this.client }))
- }
-
- private _session?: Session
- get session(): Session {
- return (this._session ??= new Session({ client: this.client }))
- }
-
- private _resource?: Resource
- get resource(): Resource {
- return (this._resource ??= new Resource({ client: this.client }))
- }
-}
-
export class Session2 extends HeyApiClient {
/**
* List sessions
@@ -3898,16 +3897,16 @@ export class OpencodeClient extends HeyApiClient {
return (this._tool ??= new Tool({ client: this.client }))
}
- private _worktree?: Worktree
- get worktree(): Worktree {
- return (this._worktree ??= new Worktree({ client: this.client }))
- }
-
private _experimental?: Experimental
get experimental(): Experimental {
return (this._experimental ??= new Experimental({ client: this.client }))
}
+ private _worktree?: Worktree
+ get worktree(): Worktree {
+ return (this._worktree ??= new Worktree({ client: this.client }))
+ }
+
private _session?: Session2
get session(): Session2 {
return (this._session ??= new Session2({ client: this.client }))
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 69d105610..afb2224a7 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -889,21 +889,6 @@ export type EventVcsBranchUpdated = {
}
}
-export type EventWorktreeReady = {
- type: "worktree.ready"
- properties: {
- name: string
- branch: string
- }
-}
-
-export type EventWorktreeFailed = {
- type: "worktree.failed"
- properties: {
- message: string
- }
-}
-
export type EventWorkspaceReady = {
type: "workspace.ready"
properties: {
@@ -957,6 +942,21 @@ export type EventPtyDeleted = {
}
}
+export type EventWorktreeReady = {
+ type: "worktree.ready"
+ properties: {
+ name: string
+ branch: string
+ }
+}
+
+export type EventWorktreeFailed = {
+ type: "worktree.failed"
+ properties: {
+ message: string
+ }
+}
+
export type Event =
| EventInstallationUpdated
| EventInstallationUpdateAvailable
@@ -995,14 +995,14 @@ export type Event =
| EventSessionDiff
| EventSessionError
| EventVcsBranchUpdated
- | EventWorktreeReady
- | EventWorktreeFailed
| EventWorkspaceReady
| EventWorkspaceFailed
| EventPtyCreated
| EventPtyUpdated
| EventPtyExited
| EventPtyDeleted
+ | EventWorktreeReady
+ | EventWorktreeFailed
export type GlobalEvent = {
directory: string
@@ -1631,6 +1631,16 @@ export type ToolListItem = {
export type ToolList = Array<ToolListItem>
+export type Workspace = {
+ id: string
+ type: string
+ branch: string | null
+ name: string | null
+ directory: string | null
+ extra: unknown | null
+ projectID: string
+}
+
export type Worktree = {
name: string
branch: string
@@ -1645,16 +1655,6 @@ export type WorktreeCreateInput = {
startCommand?: string
}
-export type Workspace = {
- id: string
- branch: string | null
- projectID: string
- config: {
- directory: string
- type: "worktree"
- }
-}
-
export type WorktreeRemoveInput = {
directory: string
}
@@ -2444,80 +2444,60 @@ export type ToolListResponses = {
export type ToolListResponse = ToolListResponses[keyof ToolListResponses]
-export type WorktreeRemoveData = {
- body?: WorktreeRemoveInput
- path?: never
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/experimental/worktree"
-}
-
-export type WorktreeRemoveErrors = {
- /**
- * Bad request
- */
- 400: BadRequestError
-}
-
-export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors]
-
-export type WorktreeRemoveResponses = {
- /**
- * Worktree removed
- */
- 200: boolean
-}
-
-export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses]
-
-export type WorktreeListData = {
+export type ExperimentalWorkspaceListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/worktree"
+ url: "/experimental/workspace"
}
-export type WorktreeListResponses = {
+export type ExperimentalWorkspaceListResponses = {
/**
- * List of worktree directories
+ * Workspaces
*/
- 200: Array<string>
+ 200: Array<Workspace>
}
-export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses]
+export type ExperimentalWorkspaceListResponse =
+ ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses]
-export type WorktreeCreateData = {
- body?: WorktreeCreateInput
+export type ExperimentalWorkspaceCreateData = {
+ body?: {
+ id?: string
+ type: string
+ branch: string | null
+ extra: unknown | null
+ }
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/worktree"
+ url: "/experimental/workspace"
}
-export type WorktreeCreateErrors = {
+export type ExperimentalWorkspaceCreateErrors = {
/**
* Bad request
*/
400: BadRequestError
}
-export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors]
+export type ExperimentalWorkspaceCreateError =
+ ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors]
-export type WorktreeCreateResponses = {
+export type ExperimentalWorkspaceCreateResponses = {
/**
- * Worktree created
+ * Workspace created
*/
- 200: Worktree
+ 200: Workspace
}
-export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses]
+export type ExperimentalWorkspaceCreateResponse =
+ ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses]
export type ExperimentalWorkspaceRemoveData = {
body?: never
@@ -2551,63 +2531,80 @@ export type ExperimentalWorkspaceRemoveResponses = {
export type ExperimentalWorkspaceRemoveResponse =
ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses]
-export type ExperimentalWorkspaceCreateData = {
- body?: {
- branch: string | null
- config: {
- directory: string
- type: "worktree"
- }
- }
- path: {
- id: string
- }
+export type WorktreeRemoveData = {
+ body?: WorktreeRemoveInput
+ path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace/{id}"
+ url: "/experimental/worktree"
}
-export type ExperimentalWorkspaceCreateErrors = {
+export type WorktreeRemoveErrors = {
/**
* Bad request
*/
400: BadRequestError
}
-export type ExperimentalWorkspaceCreateError =
- ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors]
+export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors]
-export type ExperimentalWorkspaceCreateResponses = {
+export type WorktreeRemoveResponses = {
/**
- * Workspace created
+ * Worktree removed
*/
- 200: Workspace
+ 200: boolean
}
-export type ExperimentalWorkspaceCreateResponse =
- ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses]
+export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses]
-export type ExperimentalWorkspaceListData = {
+export type WorktreeListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace"
+ url: "/experimental/worktree"
}
-export type ExperimentalWorkspaceListResponses = {
+export type WorktreeListResponses = {
/**
- * Workspaces
+ * List of worktree directories
*/
- 200: Array<Workspace>
+ 200: Array<string>
}
-export type ExperimentalWorkspaceListResponse =
- ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses]
+export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses]
+
+export type WorktreeCreateData = {
+ body?: WorktreeCreateInput
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/experimental/worktree"
+}
+
+export type WorktreeCreateErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+}
+
+export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors]
+
+export type WorktreeCreateResponses = {
+ /**
+ * Worktree created
+ */
+ 200: Worktree
+}
+
+export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses]
export type WorktreeResetData = {
body?: WorktreeResetInput
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 8f03cb92c..7db79bcbe 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -1108,9 +1108,9 @@
]
}
},
- "/experimental/worktree": {
+ "/experimental/workspace": {
"post": {
- "operationId": "worktree.create",
+ "operationId": "experimental.workspace.create",
"parameters": [
{
"in": "query",
@@ -1127,15 +1127,15 @@
}
}
],
- "summary": "Create worktree",
- "description": "Create a new git worktree for the current project and run any configured startup scripts.",
+ "summary": "Create workspace",
+ "description": "Create a workspace for the current project.",
"responses": {
"200": {
- "description": "Worktree created",
+ "description": "Workspace created",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Worktree"
+ "$ref": "#/components/schemas/Workspace"
}
}
}
@@ -1155,7 +1155,35 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorktreeCreateInput"
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^wrk.*"
+ },
+ "type": {
+ "type": "string"
+ },
+ "branch": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "extra": {
+ "anyOf": [
+ {},
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": ["type", "branch", "extra"]
}
}
}
@@ -1163,12 +1191,12 @@
"x-codeSamples": [
{
"lang": "js",
- "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.create({\n ...\n})"
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.create({\n ...\n})"
}
]
},
"get": {
- "operationId": "worktree.list",
+ "operationId": "experimental.workspace.list",
"parameters": [
{
"in": "query",
@@ -1185,17 +1213,17 @@
}
}
],
- "summary": "List worktrees",
- "description": "List all sandbox worktrees for the current project.",
+ "summary": "List workspaces",
+ "description": "List all workspaces.",
"responses": {
"200": {
- "description": "List of worktree directories",
+ "description": "Workspaces",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
- "type": "string"
+ "$ref": "#/components/schemas/Workspace"
}
}
}
@@ -1205,12 +1233,14 @@
"x-codeSamples": [
{
"lang": "js",
- "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.list({\n ...\n})"
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.list({\n ...\n})"
}
]
- },
+ }
+ },
+ "/experimental/workspace/{id}": {
"delete": {
- "operationId": "worktree.remove",
+ "operationId": "experimental.workspace.remove",
"parameters": [
{
"in": "query",
@@ -1225,17 +1255,26 @@
"schema": {
"type": "string"
}
+ },
+ {
+ "in": "path",
+ "name": "id",
+ "schema": {
+ "type": "string",
+ "pattern": "^wrk.*"
+ },
+ "required": true
}
],
- "summary": "Remove worktree",
- "description": "Remove a git worktree and delete its branch.",
+ "summary": "Remove workspace",
+ "description": "Remove an existing workspace.",
"responses": {
"200": {
- "description": "Worktree removed",
+ "description": "Workspace removed",
"content": {
"application/json": {
"schema": {
- "type": "boolean"
+ "$ref": "#/components/schemas/Workspace"
}
}
}
@@ -1251,26 +1290,17 @@
}
}
},
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/WorktreeRemoveInput"
- }
- }
- }
- },
"x-codeSamples": [
{
"lang": "js",
- "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.remove({\n ...\n})"
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.remove({\n ...\n})"
}
]
}
},
- "/experimental/workspace/{id}": {
+ "/experimental/worktree": {
"post": {
- "operationId": "experimental.workspace.create",
+ "operationId": "worktree.create",
"parameters": [
{
"in": "query",
@@ -1285,26 +1315,17 @@
"schema": {
"type": "string"
}
- },
- {
- "in": "path",
- "name": "id",
- "schema": {
- "type": "string",
- "pattern": "^wrk.*"
- },
- "required": true
}
],
- "summary": "Create workspace",
- "description": "Create a workspace for the current project.",
+ "summary": "Create worktree",
+ "description": "Create a new git worktree for the current project and run any configured startup scripts.",
"responses": {
"200": {
- "description": "Workspace created",
+ "description": "Worktree created",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Workspace"
+ "$ref": "#/components/schemas/Worktree"
}
}
}
@@ -1324,37 +1345,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "branch": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ]
- },
- "config": {
- "anyOf": [
- {
- "type": "object",
- "properties": {
- "directory": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "const": "worktree"
- }
- },
- "required": ["directory", "type"]
- }
- ]
- }
- },
- "required": ["branch", "config"]
+ "$ref": "#/components/schemas/WorktreeCreateInput"
}
}
}
@@ -1362,12 +1353,12 @@
"x-codeSamples": [
{
"lang": "js",
- "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.create({\n ...\n})"
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.create({\n ...\n})"
}
]
},
- "delete": {
- "operationId": "experimental.workspace.remove",
+ "get": {
+ "operationId": "worktree.list",
"parameters": [
{
"in": "query",
@@ -1382,36 +1373,20 @@
"schema": {
"type": "string"
}
- },
- {
- "in": "path",
- "name": "id",
- "schema": {
- "type": "string",
- "pattern": "^wrk.*"
- },
- "required": true
}
],
- "summary": "Remove workspace",
- "description": "Remove an existing workspace.",
+ "summary": "List worktrees",
+ "description": "List all sandbox worktrees for the current project.",
"responses": {
"200": {
- "description": "Workspace removed",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Workspace"
- }
- }
- }
- },
- "400": {
- "description": "Bad request",
+ "description": "List of worktree directories",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/BadRequestError"
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
}
}
@@ -1420,14 +1395,12 @@
"x-codeSamples": [
{
"lang": "js",
- "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.remove({\n ...\n})"
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.list({\n ...\n})"
}
]
- }
- },
- "/experimental/workspace": {
- "get": {
- "operationId": "experimental.workspace.list",
+ },
+ "delete": {
+ "operationId": "worktree.remove",
"parameters": [
{
"in": "query",
@@ -1444,27 +1417,43 @@
}
}
],
- "summary": "List workspaces",
- "description": "List all workspaces.",
+ "summary": "Remove worktree",
+ "description": "Remove a git worktree and delete its branch.",
"responses": {
"200": {
- "description": "Workspaces",
+ "description": "Worktree removed",
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Workspace"
- }
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/WorktreeRemoveInput"
+ }
+ }
+ }
+ },
"x-codeSamples": [
{
"lang": "js",
- "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.list({\n ...\n})"
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.remove({\n ...\n})"
}
]
}
@@ -9280,47 +9269,6 @@
},
"required": ["type", "properties"]
},
- "Event.worktree.ready": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "worktree.ready"
- },
- "properties": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "branch": {
- "type": "string"
- }
- },
- "required": ["name", "branch"]
- }
- },
- "required": ["type", "properties"]
- },
- "Event.worktree.failed": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "worktree.failed"
- },
- "properties": {
- "type": "object",
- "properties": {
- "message": {
- "type": "string"
- }
- },
- "required": ["message"]
- }
- },
- "required": ["type", "properties"]
- },
"Event.workspace.ready": {
"type": "object",
"properties": {
@@ -9472,6 +9420,47 @@
},
"required": ["type", "properties"]
},
+ "Event.worktree.ready": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "worktree.ready"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "branch": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "branch"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.worktree.failed": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "worktree.failed"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
"Event": {
"anyOf": [
{
@@ -9586,12 +9575,6 @@
"$ref": "#/components/schemas/Event.vcs.branch.updated"
},
{
- "$ref": "#/components/schemas/Event.worktree.ready"
- },
- {
- "$ref": "#/components/schemas/Event.worktree.failed"
- },
- {
"$ref": "#/components/schemas/Event.workspace.ready"
},
{
@@ -9608,6 +9591,12 @@
},
{
"$ref": "#/components/schemas/Event.pty.deleted"
+ },
+ {
+ "$ref": "#/components/schemas/Event.worktree.ready"
+ },
+ {
+ "$ref": "#/components/schemas/Event.worktree.failed"
}
]
},
@@ -11001,6 +10990,60 @@
"$ref": "#/components/schemas/ToolListItem"
}
},
+ "Workspace": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^wrk.*"
+ },
+ "type": {
+ "type": "string"
+ },
+ "branch": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "name": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "directory": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "extra": {
+ "anyOf": [
+ {},
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "projectID": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "type", "branch", "name", "directory", "extra", "projectID"]
+ },
"Worktree": {
"type": "object",
"properties": {
@@ -11028,46 +11071,6 @@
}
}
},
- "Workspace": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^wrk.*"
- },
- "branch": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ]
- },
- "projectID": {
- "type": "string"
- },
- "config": {
- "anyOf": [
- {
- "type": "object",
- "properties": {
- "directory": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "const": "worktree"
- }
- },
- "required": ["directory", "type"]
- }
- ]
- }
- },
- "required": ["id", "branch", "projectID", "config"]
- },
"WorktreeRemoveInput": {
"type": "object",
"properties": {
diff --git a/packages/slack/package.json b/packages/slack/package.json
index 72ffe20d5..cf9795762 100644
--- a/packages/slack/package.json
+++ b/packages/slack/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
- "version": "1.2.15",
+ "version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/ui/package.json b/packages/ui/package.json
index c3b2cf086..52420009f 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
- "version": "1.2.15",
+ "version": "1.2.17",
"type": "module",
"license": "MIT",
"exports": {
@@ -34,6 +34,7 @@
"@types/bun": "catalog:",
"@types/katex": "0.16.7",
"@types/luxon": "catalog:",
+ "@typescript/native-preview": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
@@ -50,7 +51,6 @@
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:",
- "@typescript/native-preview": "catalog:",
"dompurify": "3.3.1",
"fuzzysort": "catalog:",
"katex": "0.16.27",
@@ -59,7 +59,7 @@
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
"morphdom": "2.7.8",
- "motion": "12.34.3",
+ "motion": "12.34.5",
"motion-dom": "12.34.3",
"motion-utils": "12.29.2",
"remeda": "catalog:",
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index a97b38671..aecdbc8e4 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -1494,27 +1494,6 @@ ToolRegistry.register({
return `${path.slice(0, idx)}/session/${sessionId}`
})
- const handleLinkClick = (e: MouseEvent) => {
- const sessionId = childSessionId()
- const url = href()
- if (!sessionId || !url) return
-
- e.stopPropagation()
-
- if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
-
- const nav = data.navigateToSession
- if (!nav || typeof window === "undefined") return
-
- e.preventDefault()
- const before = window.location.pathname + window.location.search + window.location.hash
- nav(sessionId)
- setTimeout(() => {
- const after = window.location.pathname + window.location.search + window.location.hash
- if (after === before) window.location.assign(url)
- }, 50)
- }
-
const titleContent = () => <TextShimmer text={title()} active={running()} />
const trigger = () => (
@@ -1531,7 +1510,7 @@ ToolRegistry.register({
data-slot="basic-tool-tool-subtitle"
class="clickable subagent-link"
href={url()}
- onClick={handleLinkClick}
+ onClick={(e) => e.stopPropagation()}
>
{description()}
</a>
diff --git a/packages/ui/src/styles/base.css b/packages/ui/src/styles/base.css
index 33a245705..b5604ad61 100644
--- a/packages/ui/src/styles/base.css
+++ b/packages/ui/src/styles/base.css
@@ -86,6 +86,17 @@ a {
app-region: drag;
}
+*[data-tauri-drag-region] button,
+*[data-tauri-drag-region] a,
+*[data-tauri-drag-region] input,
+*[data-tauri-drag-region] textarea,
+*[data-tauri-drag-region] select,
+*[data-tauri-drag-region] [role="button"],
+*[data-tauri-drag-region] [role="menuitem"],
+*[data-tauri-drag-region] [contenteditable] {
+ app-region: no-drag;
+}
+
/*
Add the correct font weight in Edge and Safari.
*/
diff --git a/packages/util/package.json b/packages/util/package.json
index 36a235639..f6f2b5514 100644
--- a/packages/util/package.json
+++ b/packages/util/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
- "version": "1.2.15",
+ "version": "1.2.17",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/web/package.json b/packages/web/package.json
index daf2ad348..ee5671e3e 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
- "version": "1.2.15",
+ "version": "1.2.17",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
diff --git a/packages/web/src/content/docs/ar/ecosystem.mdx b/packages/web/src/content/docs/ar/ecosystem.mdx
index df3a4c7dd..bcd76eb10 100644
--- a/packages/web/src/content/docs/ar/ecosystem.mdx
+++ b/packages/web/src/content/docs/ar/ecosystem.mdx
@@ -6,71 +6,72 @@ description: مشاريع وتكاملات مبنية باستخدام OpenCode.
مجموعة من مشاريع المجتمع المبنية على OpenCode.
:::note
-هل تريد إضافة مشروع مرتبط بـ OpenCode إلى هذه القائمة؟ قدّم PR.
+هل تريد إضافة مشروعك المتعلق بـ OpenCode إلى هذه القائمة؟ أرسل طلب سحب (PR).
:::
-يمكنك أيضا الاطلاع على [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) و [opencode.cafe](https://opencode.cafe)؛ وهو مجتمع يجمع روابط النظام البيئي والمجتمع.
+يمكنك أيضًا الاطلاع على [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) و [opencode.cafe](https://opencode.cafe)، وهو مجتمع يجمع النظام البيئي والمجتمع.
---
## الإضافات
-| الاسم | الوصف |
-| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | تشغيل جلسات OpenCode تلقائيا داخل بيئات Daytona معزولة مع مزامنة git ومعاينات حية |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | حقن ترويسات جلسة Helicone تلقائيا لتجميع الطلبات |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | حقن أنواع TypeScript/Svelte تلقائيا في قراءات الملفات باستخدام أدوات البحث |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | استخدام اشتراك ChatGPT Plus/Pro بدلا من أرصدة واجهة API |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | استخدام خطة Gemini الحالية بدلا من فوترة API |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | استخدام نماذج Antigravity المجانية بدلا من فوترة API |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | عزل devcontainer متعدد الفروع مع استنساخات shallow ومنافذ تُعيَّن تلقائيا |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | ملحق Google Antigravity OAuth مع دعم Google Search ومعالجة API أكثر متانة |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | تحسين استخدام الرموز (tokens) عبر تقليم مخرجات الأدوات القديمة |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | إضافة دعم websearch أصلي للمزوّدين المدعومين بأسلوب مستند إلى Google |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | تمكين وكلاء الذكاء الاصطناعي من تشغيل عمليات بالخلفية داخل PTY وإرسال إدخال تفاعلي إليها. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | إرشادات لأوامر shell غير التفاعلية - تمنع التعليق الناتج عن عمليات تعتمد على TTY |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | تتبع استخدام OpenCode عبر Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | تنظيف جداول Markdown التي تنتجها نماذج LLM |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | تحرير الشيفرة أسرع بـ 10x باستخدام Morph Fast Apply API وعلامات تعديل كسولة |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | وكلاء خلفية وأدوات LSP/AST/MCP جاهزة ووكلاء منتقون وتوافق مع Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | إشعارات سطح المكتب وتنبيهات صوتية لجلسات OpenCode |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | إشعارات سطح المكتب وتنبيهات صوتية لأحداث الأذونات والإكمال والأخطاء |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | تسمية جلسات Zellij تلقائيا بالاعتماد على سياق OpenCode وبمساعدة الذكاء الاصطناعي |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | تمكين وكلاء OpenCode من تحميل الموجهات عند الطلب عبر اكتشاف المهارات وحقنها |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | ذاكرة مستمرة عبر الجلسات باستخدام Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | مراجعة تفاعلية للخطة مع تعليقات توضيحية مرئية ومشاركة خاصة/دون اتصال |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | توسيع /commands في opencode إلى نظام تنسيق قوي مع تحكم دقيق في التدفق |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | جدولة مهام متكررة باستخدام launchd (Mac) أو systemd (Linux) بصياغة cron |
-| [micode](https://github.com/vtemian/micode) | سير عمل منظم: عصف ذهني → خطة → تنفيذ مع استمرارية الجلسة |
-| [octto](https://github.com/vtemian/octto) | واجهة متصفح تفاعلية للعصف الذهني بالذكاء الاصطناعي مع نماذج متعددة الأسئلة |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | وكلاء خلفية على نمط Claude Code مع تفويض غير متزامن واستمرارية للسياق |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | إشعارات نظام تشغيل أصلية لـ OpenCode - اعرف متى تكتمل المهام |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | حزمة تنسيق متعددة الوكلاء - 16 مكوّنا، تثبيت واحد |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | git worktrees بلا تعقيد لـ OpenCode |
+| الاسم | الوصف |
+| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | تشغيل جلسات OpenCode تلقائيًا في صناديق حماية Daytona معزولة مع مزامنة git ومعاينات حية |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | حقن ترويسات جلسة Helicone تلقائيًا لتجميع الطلبات |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | حقن أنواع TypeScript/Svelte تلقائيًا في عمليات قراءة الملفات باستخدام أدوات البحث |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | استخدام اشتراك ChatGPT Plus/Pro الخاص بك بدلاً من أرصدة API |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | استخدام خطة Gemini الحالية الخاصة بك بدلاً من فوترة API |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | استخدام نماذج Antigravity المجانية بدلاً من فوترة API |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | عزل devcontainer متعدد الفروع مع نسخ shallow ومنافذ معينة تلقائيًا |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | إضافة Google Antigravity OAuth، مع دعم بحث Google، ومعالجة API أكثر قوة |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | تحسين استخدام التوكنات عن طريق تقليم مخرجات الأدوات القديمة |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | تنقيح الأسرار/بيانات التعريف الشخصي (PII) إلى نصوص بديلة بأسلوب VibeGuard قبل استدعاءات LLM؛ واستعادتها محليًا |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | إضافة دعم بحث الويب الأصلي للموفرين المدعومين بأسلوب Google grounded |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | تمكين وكلاء الذكاء الاصطناعي من تشغيل عمليات الخلفية في PTY، وإرسال مدخلات تفاعلية إليها. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | تعليمات لأوامر shell غير التفاعلية - تمنع التعليق الناتج عن العمليات المعتمدة على TTY |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | تتبع استخدام OpenCode باستخدام Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | تنظيف جداول markdown التي تنتجها LLMs |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | تحرير الكود أسرع بـ 10 مرات باستخدام Morph Fast Apply API وعلامات التحرير الكسولة |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | وكلاء الخلفية، وأدوات LSP/AST/MCP المعدة مسبقًا، ووكلاء مختارون، متوافق مع Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | إشعارات سطح المكتب وتنبيهات صوتية لجلسات OpenCode |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | إشعارات سطح المكتب وتنبيهات صوتية لأحداث الإذن والاكتمال والخطأ |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | تسمية جلسات Zellij تلقائيًا بدعم الذكاء الاصطناعي بناءً على سياق OpenCode |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | السماح لوكلاء OpenCode بتحميل المطالبات (prompts) بشكل كسول عند الطلب مع اكتشاف المهارات وحقنها |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | ذاكرة مستمرة عبر الجلسات باستخدام Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | مراجعة تفاعلية للخطة مع تعليقات توضيحية مرئية ومشاركة خاصة/بدون اتصال |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | توسيع opencode /commands إلى نظام تنسيق قوي مع تحكم دقيق في التدفق |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | جدولة الوظائف المتكررة باستخدام launchd (Mac) أو systemd (Linux) بصيغة cron |
+| [micode](https://github.com/vtemian/micode) | سير عمل منظم: عصف ذهني ← تخطيط ← تنفيذ مع استمرارية الجلسة |
+| [octto](https://github.com/vtemian/octto) | واجهة مستخدم تفاعلية للمتصفح للعصف الذهني بالذكاء الاصطناعي مع نماذج متعددة الأسئلة |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | وكلاء خلفية بأسلوب Claude Code مع تفويض غير متزامن واستمرارية السياق |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | إشعارات نظام التشغيل الأصلية لـ OpenCode – اعرف متى تكتمل المهام |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | حزمة تنسيق متعددة الوكلاء – 16 مكونًا، تثبيت واحد |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | أشجار عمل git (worktrees) خالية من الاحتكاك لـ OpenCode |
---
## المشاريع
-| الاسم | الوصف |
-| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
-| [kimaki](https://github.com/remorses/kimaki) | بوت Discord للتحكم بجلسات OpenCode، مبني على SDK |
-| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | ملحق Neovim لموجهات تراعي المحرر، مبني على API |
-| [portal](https://github.com/hosenur/portal) | واجهة ويب تركز على الجوال لـ OpenCode عبر Tailscale/VPN |
-| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | قالب لبناء ملحقات OpenCode |
-| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | واجهة Neovim لـ opencode - وكيل برمجة بالذكاء الاصطناعي يعمل في terminal |
-| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | موفر Vercel AI SDK لاستخدام OpenCode عبر @opencode-ai/sdk |
-| [OpenChamber](https://github.com/btriapitsyn/openchamber) | تطبيق ويب/سطح مكتب وامتداد VS Code لـ OpenCode |
-| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | ملحق Obsidian يدمج OpenCode داخل واجهة Obsidian |
-| [OpenWork](https://github.com/different-ai/openwork) | بديل مفتوح المصدر لـ Claude Cowork، مدعوم بـ OpenCode |
-| [ocx](https://github.com/kdcokenny/ocx) | مدير امتدادات OpenCode مع ملفات تعريف محمولة ومعزولة. |
-| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | تطبيق عميل لسطح المكتب والويب والجوال وعن بُعد لـ OpenCode |
+| الاسم | الوصف |
+| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
+| [kimaki](https://github.com/remorses/kimaki) | بوت Discord للتحكم في جلسات OpenCode، مبني على SDK |
+| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | إضافة Neovim للمطالبات المدركة للمحرر، مبنية على API |
+| [portal](https://github.com/hosenur/portal) | واجهة ويب مخصصة للجوال لـ OpenCode عبر Tailscale/VPN |
+| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | قالب لبناء إضافات OpenCode |
+| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | واجهة Neovim لـ opencode - وكيل برمجة بالذكاء الاصطناعي يعتمد على الطرفية |
+| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | موفر Vercel AI SDK لاستخدام OpenCode عبر @opencode-ai/sdk |
+| [OpenChamber](https://github.com/btriapitsyn/openchamber) | تطبيق ويب / سطح مكتب وامتداد VS Code لـ OpenCode |
+| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | إضافة Obsidian تدمج OpenCode في واجهة مستخدم Obsidian |
+| [OpenWork](https://github.com/different-ai/openwork) | بديل مفتوح المصدر لـ Claude Cowork، مدعوم بواسطة OpenCode |
+| [ocx](https://github.com/kdcokenny/ocx) | مدير امتدادات OpenCode مع ملفات تعريف محمولة ومعزولة. |
+| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | تطبيق عميل لسطح المكتب والويب والجوال وعن بعد لـ OpenCode |
---
## الوكلاء
-| الاسم | الوصف |
-| ----------------------------------------------------------------- | --------------------------------------------- |
-| [Agentic](https://github.com/Cluster444/agentic) | وكلاء وأوامر ذكاء اصطناعي معيارية لتطوير منظم |
-| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | إعدادات وموجهات ووكلاء وملحقات لسير عمل محسّن |
+| الاسم | الوصف |
+| ----------------------------------------------------------------- | ------------------------------------------------ |
+| [Agentic](https://github.com/Cluster444/agentic) | وكلاء ذكاء اصطناعي وأوامر معيارية للتطوير المنظم |
+| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | تكوينات، ومطالبات، ووكلاء، وإضافات لسير عمل محسن |
diff --git a/packages/web/src/content/docs/bs/ecosystem.mdx b/packages/web/src/content/docs/bs/ecosystem.mdx
index c7dea0c6e..be4816f8f 100644
--- a/packages/web/src/content/docs/bs/ecosystem.mdx
+++ b/packages/web/src/content/docs/bs/ecosystem.mdx
@@ -4,45 +4,50 @@ description: Projekti i integracije izgrađeni uz OpenCode.
---
Kolekcija projekata zajednice izgrađenih na OpenCode.
+
:::note
Želite li na ovu listu dodati svoj OpenCode projekat? Pošaljite PR.
:::
+
Također možete pogledati [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) i [opencode.cafe](https://opencode.cafe), zajednicu koja spaja ekosistem i zajednicu.
---
## Dodaci
-| Ime | Opis |
-| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Automatski pokrenite OpenCode sesije u izoliranim Daytona sandboxovima uz git sinhronizaciju i preglede uživo |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatski ubacite Helicone zaglavlja sesije za grupisanje zahtjeva |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Automatski ubaci TypeScript/Svelte tipove u čitanje datoteka pomoću alata za pretraživanje |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Koristite svoju ChatGPT Plus/Pro pretplatu umjesto API kredita |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Koristite svoj postojeći Gemini plan umjesto API naplate |
-| [opencodentigravity-auth](https://github.com/NoeFabris/opencodentigravity-auth) | Koristite besplatne modele Antigravity umjesto API naplate |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Izolacija devcontainer-a s više grana s plitkim klonovima i automatski dodijeljenim portovima |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth dodatak, s podrškom za Google pretraživanje i robusnijim API rukovanjem |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimizirajte korištenje tokena smanjenjem izlaza zastarjelih alata |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Dodajte podršku za izvorno web pretraživanje za podržane provajdere sa stilom utemeljenim na Googleu |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Omogućuje AI agentima da pokreću pozadinske procese u PTY-u, šalju im interaktivni ulaz. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Upute za neinteraktivne naredbe ljuske - sprječava visi od TTY ovisnih operacija | | [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Pratite upotrebu OpenCode sa Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Očistite tabele umanjenja vrijednosti koje su izradili LLM |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x brže uređivanje koda s Morph Fast Apply API-jem i markerima za lijeno uređivanje |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Pozadinski agenti, unapred izgrađeni LSP/AST/MCP alati, kurirani agenti, kompatibilni sa Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Obavještenja na radnoj površini i zvučna upozorenja za OpenCode sesije |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Obavještenja na radnoj površini i zvučna upozorenja za dozvole, završetak i događaje greške |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Automatsko imenovanje Zellij sesije na bazi OpenCode konteksta |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Dozvolite OpenCode agentima da lijeno učitavaju upite na zahtjev uz otkrivanje vještina i ubrizgavanje |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Trajna memorija kroz sesije koristeći Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktivni pregled plana s vizualnim napomenama i privatnim/offline dijeljenjem |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Proširite opencode /komande u moćan sistem orkestracije sa granularnom kontrolom toka |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planirajte ponavljajuće poslove koristeći launchd (Mac) ili systemd (Linux) sa cron sintaksom | | [micode](https://github.com/vtemian/micode) | Strukturirana Brainstorm → Plan → Implementacija toka rada uz kontinuitet sesije |
-| [octto](https://github.com/vtemian/octto) | Interaktivno korisničko sučelje pretraživača za AI brainstorming sa obrascima za više pitanja |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Pozadinski agenti u stilu Claudea s asinhroniziranim delegiranjem i postojanošću konteksta |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notifikacije izvornog OS-a za OpenCode – znajte kada se zadaci dovrše |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Uvezeni višeagentni orkestracijski pojas – 16 komponenti, jedna instalacija |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Git radna stabla bez trenja za OpenCode |
+| Ime | Opis |
+| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Automatski pokrenite OpenCode sesije u izoliranim Daytona sandboxovima uz git sinhronizaciju i preglede uživo |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatski ubacite Helicone zaglavlja sesije za grupisanje zahtjeva |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Automatski ubaci TypeScript/Svelte tipove u čitanje datoteka pomoću alata za pretraživanje |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Koristite svoju ChatGPT Plus/Pro pretplatu umjesto API kredita |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Koristite svoj postojeći Gemini plan umjesto API naplate |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Koristite besplatne modele Antigravity umjesto API naplate |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Izolacija devcontainer-a s više grana s plitkim klonovima i automatski dodijeljenim portovima |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth dodatak, s podrškom za Google pretraživanje i robusnijim API rukovanjem |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimizirajte korištenje tokena smanjenjem izlaza zastarjelih alata |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Redigujte tajne/PII u rezervirana mjesta u stilu VibeGuarda prije LLM poziva; vratite lokalno |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Dodajte podršku za izvorno web pretraživanje za podržane provajdere sa stilom utemeljenim na Googleu |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Omogućuje AI agentima da pokreću pozadinske procese u PTY-u, šalju im interaktivni ulaz. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Upute za neinteraktivne naredbe ljuske - sprječava visi od TTY ovisnih operacija |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Pratite upotrebu OpenCode sa Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Očistite tabele umanjenja vrijednosti koje su izradili LLM |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x brže uređivanje koda s Morph Fast Apply API-jem i markerima za lijeno uređivanje |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Pozadinski agenti, unapred izgrađeni LSP/AST/MCP alati, kurirani agenti, kompatibilni sa Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Obavještenja na radnoj površini i zvučna upozorenja za OpenCode sesije |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Obavještenja na radnoj površini i zvučna upozorenja za dozvole, završetak i događaje greške |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Automatsko imenovanje Zellij sesije na bazi OpenCode konteksta |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Dozvolite OpenCode agentima da lijeno učitavaju upite na zahtjev uz otkrivanje vještina i ubrizgavanje |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Trajna memorija kroz sesije koristeći Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktivni pregled plana s vizualnim napomenama i privatnim/offline dijeljenjem |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Proširite opencode /komande u moćan sistem orkestracije sa granularnom kontrolom toka |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planirajte ponavljajuće poslove koristeći launchd (Mac) ili systemd (Linux) sa cron sintaksom |
+| [micode](https://github.com/vtemian/micode) | Strukturirana Brainstorm → Plan → Implementacija toka rada uz kontinuitet sesije |
+| [octto](https://github.com/vtemian/octto) | Interaktivno korisničko sučelje pretraživača za AI brainstorming sa obrascima za više pitanja |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Pozadinski agenti u stilu Claudea s asinhroniziranim delegiranjem i postojanošću konteksta |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notifikacije izvornog OS-a za OpenCode – znajte kada se zadaci dovrše |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Uvezeni višeagentni orkestracijski pojas – 16 komponenti, jedna instalacija |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Git radna stabla bez trenja za OpenCode |
---
@@ -55,7 +60,7 @@ Također možete pogledati [awesome-opencode](https://github.com/awesome-opencod
| [portal](https://github.com/hosenur/portal) | Mobilni korisnički interfejs za OpenCode preko Tailscale/VPN |
| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Predložak za izgradnju OpenCode dodataka |
| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Neovim frontend za opencode - terminal baziran AI agent za kodiranje |
-| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Vercel AI SDK dobavljač za korištenje OpenCode putem @opencodei/sdk |
+| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Vercel AI SDK dobavljač za korištenje OpenCode putem @opencode-ai/sdk |
| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Web / Desktop App i VS Code Extension za OpenCode |
| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Obsidian dodatak koji ugrađuje OpenCode u Obsidian-ov UI |
| [OpenWork](https://github.com/different-ai/openwork) | Alternativa otvorenog koda Claudeu Coworku, pokretana pomoću OpenCode |
@@ -66,7 +71,7 @@ Također možete pogledati [awesome-opencode](https://github.com/awesome-opencod
## Agenti
-| Ime | Opis |
-| ------------------------------------------------------------- | --------------------------------------------------------------- |
-| [Agentic](https://github.com/Cluster444/agentic) | Modularni AI agenti i komande za strukturirani razvoj |
-| [opencodegents](https://github.com/darrenhinde/opencodegents) | Konfiguracije, upiti, agenti i dodaci za poboljšane tokove rada |
+| Ime | Opis |
+| ----------------------------------------------------------------- | --------------------------------------------------------------- |
+| [Agentic](https://github.com/Cluster444/agentic) | Modularni AI agenti i komande za strukturirani razvoj |
+| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | Konfiguracije, upiti, agenti i dodaci za poboljšane tokove rada |
diff --git a/packages/web/src/content/docs/da/ecosystem.mdx b/packages/web/src/content/docs/da/ecosystem.mdx
index 8da7e42d1..e7111a519 100644
--- a/packages/web/src/content/docs/da/ecosystem.mdx
+++ b/packages/web/src/content/docs/da/ecosystem.mdx
@@ -15,38 +15,39 @@ Du kan også tjekke [awesome-opencode](https://github.com/awesome-opencode/aweso
## Plugins
-| Navn | Beskrivelse |
-| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Kør automatisk OpenCode-sessioner i isolerede Daytona-sandkasser med git-synkronisering og live forhåndsvisninger |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Injicer automatisk Helicone-sessionsoverskrifter til anmodningsgruppering |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Autoinjicer TypeScript/Svelte-typer i fillæsninger med opslagsværktøjer |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Brug dit ChatGPT Plus/Pro abonnement i stedet for API kreditter |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Brug din eksisterende Gemini-plan i stedet for API fakturering |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Brug Antigravitys gratis modeller i stedet for API fakturering |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-branch devcontainer isolation med lavvandede kloner og automatisk tildelte porte |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, med understøttelse af Google søgning og mere robust API håndtering |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimer tokenbrug ved at beskære forældede værktøjsoutput |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Tilføj native websearch-understøttelse for understøttede udbydere med Google jordet stil |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Gør det muligt for AI-agenter at køre baggrundsprocesser i en PTY, send interaktive input til dem. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instruktioner til ikke-interaktive shell-kommandoer - forhindrer hænger fra TTY-afhængige operationer |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Spor OpenCode brug med Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Ryd op afmærkningstabeller produceret af LLMs |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x hurtigere koderedigering med Morph Fast Apply API og dovne redigeringsmarkører |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Baggrundsagenter, præbyggede LSP/AST/MCP værktøjer, kuraterede agenter, Claude Kodekompatibel |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Skrivebordsmeddelelser og lydadvarsler for OpenCode-sessioner |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Skrivebordsmeddelelser og lydadvarsler for tilladelser, fuldførelse og fejlhændelser |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-drevet automatisk Zellij-sessionsnavngivning baseret på OpenCode-kontekst |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Tillad OpenCode-agenter til dovne load-prompter på efterspørgsel med færdighedsopdagelse og -injektion |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Vedvarende hukommelse på tværs af sessioner ved hjælp af Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktiv plangennemgang med visuel annotering og private/offline deling |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Udvid opencode /commands til et kraftfuldt orkestreringssystem med granulær flowkontrol |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planlæg tilbagevendende job ved hjælp af launchd (Mac) eller systemd (Linux) med cron-syntaks |
-| [micode](https://github.com/vtemian/micode) | Struktureret brainstorm → Plan → Implementer workflow med session kontinuitet |
-| [octto](https://github.com/vtemian/octto) | Interaktiv browser-UI til AI-brainstorming med formularer med flere spørgsmål |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Baggrundsagenter i kodestil med asynkron-delegering og kontekstvedholdenhed |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Native OS-meddelelser for OpenCode – ved, hvornår opgaver er fuldført |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Bundled multi-agent orkestreringssele – 16 komponenter, én installation |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Nulfriktions git-arbejdstræer for OpenCode |
+| Navn | Beskrivelse |
+| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Kør automatisk OpenCode-sessioner i isolerede Daytona-sandkasser med git-synkronisering og live forhåndsvisninger |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Injicer automatisk Helicone-sessionsoverskrifter til anmodningsgruppering |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Autoinjicer TypeScript/Svelte-typer i fillæsninger med opslagsværktøjer |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Brug dit ChatGPT Plus/Pro abonnement i stedet for API kreditter |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Brug din eksisterende Gemini-plan i stedet for API fakturering |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Brug Antigravitys gratis modeller i stedet for API fakturering |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-branch devcontainer isolation med lavvandede kloner og automatisk tildelte porte |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, med understøttelse af Google søgning og mere robust API håndtering |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimer tokenbrug ved at beskære forældede værktøjsoutput |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Masker hemmeligheder/PII til pladsholdere i VibeGuard-stil før LLM-kald; gendan lokalt |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Tilføj native websearch-understøttelse for understøttede udbydere med Google jordet stil |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Gør det muligt for AI-agenter at køre baggrundsprocesser i en PTY, send interaktive input til dem. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instruktioner til ikke-interaktive shell-kommandoer - forhindrer hænger fra TTY-afhængige operationer |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Spor OpenCode brug med Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Ryd op afmærkningstabeller produceret af LLMs |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x hurtigere koderedigering med Morph Fast Apply API og dovne redigeringsmarkører |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Baggrundsagenter, præbyggede LSP/AST/MCP værktøjer, kuraterede agenter, Claude Kodekompatibel |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Skrivebordsmeddelelser og lydadvarsler for OpenCode-sessioner |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Skrivebordsmeddelelser og lydadvarsler for tilladelser, fuldførelse og fejlhændelser |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-drevet automatisk Zellij-sessionsnavngivning baseret på OpenCode-kontekst |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Tillad OpenCode-agenter til dovne load-prompter på efterspørgsel med færdighedsopdagelse og -injektion |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Vedvarende hukommelse på tværs af sessioner ved hjælp af Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktiv plangennemgang med visuel annotering og private/offline deling |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Udvid opencode /commands til et kraftfuldt orkestreringssystem med granulær flowkontrol |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planlæg tilbagevendende job ved hjælp af launchd (Mac) eller systemd (Linux) med cron-syntaks |
+| [micode](https://github.com/vtemian/micode) | Struktureret brainstorm → Plan → Implementer workflow med session kontinuitet |
+| [octto](https://github.com/vtemian/octto) | Interaktiv browser-UI til AI-brainstorming med formularer med flere spørgsmål |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Baggrundsagenter i kodestil med asynkron-delegering og kontekstvedholdenhed |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Native OS-meddelelser for OpenCode – ved, hvornår opgaver er fuldført |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Bundled multi-agent orkestreringssele – 16 komponenter, én installation |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Nulfriktions git-arbejdstræer for OpenCode |
---
diff --git a/packages/web/src/content/docs/de/ecosystem.mdx b/packages/web/src/content/docs/de/ecosystem.mdx
index ea1bc589a..356de3832 100644
--- a/packages/web/src/content/docs/de/ecosystem.mdx
+++ b/packages/web/src/content/docs/de/ecosystem.mdx
@@ -15,38 +15,39 @@ Sie können sich auch [awesome-opencode](https://github.com/awesome-opencode/awe
## Plugins
-| Name | Beschreibung |
-| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Führen Sie OpenCode-Sitzungen automatisch in isolierten Daytona-Sandboxes mit Git-Synchronisierung und Live-Vorschauen aus |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Helicone-Sitzungsheader für die Anforderungsgruppierung automatisch einfügen |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | TypeScript/Svelte-Typen mit Suchtools automatisch in Dateilesevorgänge einfügen |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Verwenden Sie Ihr ChatGPT Plus/Pro-Abonnement anstelle von API Credits |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Verwenden Sie Ihren bestehenden Gemini-Plan anstelle der API-Abrechnung |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Nutzen Sie die kostenlosen Modelle von Antigravity anstelle der API-Abrechnung |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-Branch-Devcontainer-Isolierung mit flachen Klonen und automatisch zugewiesenen Ports |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth-Plugin mit Unterstützung für die Google-Suche und robustere API-Verarbeitung |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimieren Sie die Token-Nutzung, indem Sie veraltete Tool-Ausgaben bereinigen |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Fügen Sie native Websuchunterstützung für unterstützte Anbieter mit Google Grounded Style hinzu |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Ermöglicht AI-Agenten, Hintergrundprozesse in einem PTY auszuführen und ihnen interaktive Eingaben zu senden. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Anweisungen für nicht interaktive Shell-Befehle – verhindert Abstürze bei TTY-abhängigen Vorgängen |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Verfolgen Sie die Nutzung von OpenCode mit Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Von LLMs erstellte Abschriftentabellen bereinigen |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x schnellere Codebearbeitung mit Morph Fast Apply API und Lazy-Edit-Markern |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Hintergrundagenten, vorgefertigte LSP/AST/MCP-Tools, kuratierte Agenten, Claude Code-kompatibel |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop-Benachrichtigungen und akustische Warnungen für OpenCode-Sitzungen |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop-Benachrichtigungen und akustische Warnungen für Berechtigungs-, Abschluss- und Fehlerereignisse |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-gestützte automatische Benennung von Zellij-Sitzungen basierend auf dem OpenCode-Kontext |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Ermöglichen Sie OpenCode-Agenten das verzögerte Laden von Eingabeaufforderungen bei Bedarf mit Skill-Erkennung und -Injektion |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistenter Speicher über Sitzungen hinweg mit Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktive Planüberprüfung mit visueller Anmerkung und private/offline-Freigabe |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Erweitern Sie OpenCode /commands zu einem leistungsstarken Orchestrierungssystem mit granularer Flusskontrolle |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planen Sie wiederkehrende Jobs mit launchd (Mac) oder systemd (Linux) mit Cron-Syntax |
-| [micode](https://github.com/vtemian/micode) | Strukturiertes Brainstorming → Planen → Workflow mit Sitzungskontinuität Implementierung |
-| [octto](https://github.com/vtemian/octto) | Interaktiver Browser UI für AI Brainstorming mit Formularen mit mehreren Fragen |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Hintergrundagenten im Claude Code-Stil mit asynchroner Delegation und Kontextpersistenz |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Native OS-Benachrichtigungen für OpenCode – wissen, wann Aufgaben erledigt sind |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Gebündelter Multi-Agent-Orchestrierungs-Harness – 16 Komponenten, eine Installation |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Reibungslose Git-Arbeitsbäume für OpenCode |
+| Name | Beschreibung |
+| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Führen Sie OpenCode-Sitzungen automatisch in isolierten Daytona-Sandboxes mit Git-Synchronisierung und Live-Vorschauen aus |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Helicone-Sitzungsheader für die Anforderungsgruppierung automatisch einfügen |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | TypeScript/Svelte-Typen mit Suchtools automatisch in Dateilesevorgänge einfügen |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Verwenden Sie Ihr ChatGPT Plus/Pro-Abonnement anstelle von API Credits |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Verwenden Sie Ihren bestehenden Gemini-Plan anstelle der API-Abrechnung |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Nutzen Sie die kostenlosen Modelle von Antigravity anstelle der API-Abrechnung |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-Branch-Devcontainer-Isolierung mit flachen Klonen und automatisch zugewiesenen Ports |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth-Plugin mit Unterstützung für die Google-Suche und robustere API-Verarbeitung |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimieren Sie die Token-Nutzung, indem Sie veraltete Tool-Ausgaben bereinigen |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Schwärzen Sie Geheimnisse/PII in VibeGuard-ähnliche Platzhalter vor LLM-Aufrufen; lokal wiederherstellen |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Fügen Sie native Websuchunterstützung für unterstützte Anbieter mit Google Grounded Style hinzu |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Ermöglicht AI-Agenten, Hintergrundprozesse in einem PTY auszuführen und ihnen interaktive Eingaben zu senden. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Anweisungen für nicht interaktive Shell-Befehle – verhindert Abstürze bei TTY-abhängigen Vorgängen |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Verfolgen Sie die Nutzung von OpenCode mit Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Von LLMs erstellte Abschriftentabellen bereinigen |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x schnellere Codebearbeitung mit Morph Fast Apply API und Lazy-Edit-Markern |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Hintergrundagenten, vorgefertigte LSP/AST/MCP-Tools, kuratierte Agenten, Claude Code-kompatibel |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop-Benachrichtigungen und akustische Warnungen für OpenCode-Sitzungen |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop-Benachrichtigungen und akustische Warnungen für Berechtigungs-, Abschluss- und Fehlerereignisse |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-gestützte automatische Benennung von Zellij-Sitzungen basierend auf dem OpenCode-Kontext |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Ermöglichen Sie OpenCode-Agenten das verzögerte Laden von Eingabeaufforderungen bei Bedarf mit Skill-Erkennung und -Injektion |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistenter Speicher über Sitzungen hinweg mit Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktive Planüberprüfung mit visueller Anmerkung und private/offline-Freigabe |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Erweitern Sie OpenCode /commands zu einem leistungsstarken Orchestrierungssystem mit granularer Flusskontrolle |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planen Sie wiederkehrende Jobs mit launchd (Mac) oder systemd (Linux) mit Cron-Syntax |
+| [micode](https://github.com/vtemian/micode) | Strukturiertes Brainstorming → Planen → Workflow mit Sitzungskontinuität Implementierung |
+| [octto](https://github.com/vtemian/octto) | Interaktiver Browser UI für AI Brainstorming mit Formularen mit mehreren Fragen |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Hintergrundagenten im Claude Code-Stil mit asynchroner Delegation und Kontextpersistenz |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Native OS-Benachrichtigungen für OpenCode – wissen, wann Aufgaben erledigt sind |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Gebündelter Multi-Agent-Orchestrierungs-Harness – 16 Komponenten, eine Installation |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Reibungslose Git-Arbeitsbäume für OpenCode |
---
diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx
index 4a4205f31..4312a2b47 100644
--- a/packages/web/src/content/docs/ecosystem.mdx
+++ b/packages/web/src/content/docs/ecosystem.mdx
@@ -15,39 +15,39 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
## Plugins
-| Name | Description |
-| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Automatically run OpenCode sessions in isolated Daytona sandboxes with git sync and live previews |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-branch devcontainer isolation with shallow clones and auto-assigned ports |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
-| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Redact secrets/PII into VibeGuard-style placeholders before LLM calls; restore locally |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions for non-interactive shell commands - prevents hangs from TTY-dependent operations |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistent memory across sessions using Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interactive plan review with visual annotation and private/offline sharing |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Extend opencode /commands into a powerful orchestration system with granular flow control |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Schedule recurring jobs using launchd (Mac) or systemd (Linux) with cron syntax |
-| [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → Implement workflow with session continuity |
-| [octto](https://github.com/vtemian/octto) | Interactive browser UI for AI brainstorming with multi-question forms |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code-style background agents with async delegation and context persistence |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Native OS notifications for OpenCode – know when tasks complete |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Bundled multi-agent orchestration harness – 16 components, one install |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Zero-friction git worktrees for OpenCode |
+| Name | Description |
+| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Automatically run OpenCode sessions in isolated Daytona sandboxes with git sync and live previews |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-branch devcontainer isolation with shallow clones and auto-assigned ports |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Redact secrets/PII into VibeGuard-style placeholders before LLM calls; restore locally |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions for non-interactive shell commands - prevents hangs from TTY-dependent operations |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistent memory across sessions using Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interactive plan review with visual annotation and private/offline sharing |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Extend opencode /commands into a powerful orchestration system with granular flow control |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Schedule recurring jobs using launchd (Mac) or systemd (Linux) with cron syntax |
+| [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → Implement workflow with session continuity |
+| [octto](https://github.com/vtemian/octto) | Interactive browser UI for AI brainstorming with multi-question forms |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code-style background agents with async delegation and context persistence |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Native OS notifications for OpenCode – know when tasks complete |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Bundled multi-agent orchestration harness – 16 components, one install |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Zero-friction git worktrees for OpenCode |
---
diff --git a/packages/web/src/content/docs/es/ecosystem.mdx b/packages/web/src/content/docs/es/ecosystem.mdx
index b1f4bdc9a..dcd207137 100644
--- a/packages/web/src/content/docs/es/ecosystem.mdx
+++ b/packages/web/src/content/docs/es/ecosystem.mdx
@@ -15,38 +15,39 @@ También puedes consultar [awesome-opencode](https://github.com/awesome-opencode
## Complementos
-| Nombre | Descripción |
-| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Ejecute automáticamente sesiones OpenCode en entornos sandbox aislados de Daytona con git sync y vistas previas en vivo |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Inyecte automáticamente encabezados de sesión de Helicone para agrupación de solicitudes |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Inyecte automáticamente tipos TypeScript/Svelte en lecturas de archivos con herramientas de búsqueda |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Utilice su suscripción ChatGPT Plus/Pro en lugar de créditos API |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Utilice su plan Gemini existente en lugar de la facturación API |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Utilice los modelos gratuitos de Antigravity en lugar de la facturación API |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Aislamiento de contenedores de desarrollo de múltiples ramas con clones superficiales y puertos asignados automáticamente |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Complemento Google Antigravity OAuth, compatible con la Búsqueda de Google y manejo más sólido de API |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimice el uso de tokens eliminando los resultados de herramientas obsoletas |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Agregue soporte de búsqueda web nativa para proveedores compatibles con el estilo basado en Google |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Permite a los agentes de IA ejecutar procesos en segundo plano en un PTY y enviarles información interactiva. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instrucciones para comandos de shell no interactivos: evita bloqueos de operaciones dependientes de TTY |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Seguimiento del uso de OpenCode con Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Limpiar tablas de Markdown producidas por LLMs |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Edición de código 10 veces más rápida con Morph Fast Apply API y marcadores de edición diferidos |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agentes en segundo plano, herramientas LSP/AST/MCP prediseñadas, agentes seleccionados, compatible con Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notificaciones de escritorio y alertas sonoras para sesiones OpenCode |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notificaciones de escritorio y alertas sonoras para eventos de permiso, finalización y error |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Nomenclatura automática de sesiones Zellij impulsada por IA basada en el contexto OpenCode |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Permitir que los agentes OpenCode carguen mensajes de forma diferida a pedido con descubrimiento e inyección de habilidades |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Memoria persistente entre sesiones utilizando Supermemoria |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Revisión interactiva del plan con anotaciones visuales y uso compartido privado/sin conexión |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Amplíe opencode /commands a un potente sistema de orquestación con control de flujo granular |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Programe trabajos recurrentes usando launchd (Mac) o systemd (Linux) con sintaxis cron |
-| [micode](https://github.com/vtemian/micode) | Lluvia de ideas estructurada → Planificar → Implementar flujo de trabajo con continuidad de sesión |
-| [octto](https://github.com/vtemian/octto) | Interfaz de usuario interactiva del navegador para lluvia de ideas de IA con formularios de preguntas múltiples |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agentes en segundo plano estilo Claude Code con delegación asíncrona y persistencia de contexto |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notificaciones nativas del sistema operativo para OpenCode: sepa cuándo se completan las tareas |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Arnés de orquestación multiagente incluido: 16 componentes, una instalación |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Árboles de trabajo de Git de fricción cero para OpenCode |
+| Nombre | Descripción |
+| -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Ejecute automáticamente sesiones OpenCode en entornos sandbox aislados de Daytona con git sync y vistas previas en vivo |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Inyecte automáticamente encabezados de sesión de Helicone para agrupación de solicitudes |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Inyecte automáticamente tipos TypeScript/Svelte en lecturas de archivos con herramientas de búsqueda |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Utilice su suscripción ChatGPT Plus/Pro en lugar de créditos API |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Utilice su plan Gemini existente en lugar de la facturación API |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Utilice los modelos gratuitos de Antigravity en lugar de la facturación API |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Aislamiento de contenedores de desarrollo de múltiples ramas con clones superficiales y puertos asignados automáticamente |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Complemento Google Antigravity OAuth, compatible con la Búsqueda de Google y manejo más sólido de API |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimice el uso de tokens eliminando los resultados de herramientas obsoletas |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Redacta secretos/PII en marcadores de posición estilo VibeGuard antes de las llamadas a LLM; restaura localmente |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Agregue soporte de búsqueda web nativa para proveedores compatibles con el estilo basado en Google |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Permite a los agentes de IA ejecutar procesos en segundo plano en un PTY y enviarles información interactiva. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instrucciones para comandos de shell no interactivos: evita bloqueos de operaciones dependientes de TTY |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Seguimiento del uso de OpenCode con Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Limpiar tablas de Markdown producidas por LLMs |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Edición de código 10 veces más rápida con Morph Fast Apply API y marcadores de edición diferidos |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agentes en segundo plano, herramientas LSP/AST/MCP prediseñadas, agentes seleccionados, compatible con Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notificaciones de escritorio y alertas sonoras para sesiones OpenCode |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notificaciones de escritorio y alertas sonoras para eventos de permiso, finalización y error |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Nomenclatura automática de sesiones Zellij impulsada por IA basada en el contexto OpenCode |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Permitir que los agentes OpenCode carguen mensajes de forma diferida a pedido con descubrimiento e inyección de habilidades |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Memoria persistente entre sesiones utilizando Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Revisión interactiva del plan con anotaciones visuales y uso compartido privado/sin conexión |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Amplíe opencode /commands a un potente sistema de orquestación con control de flujo granular |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Programe trabajos recurrentes usando launchd (Mac) o systemd (Linux) con sintaxis cron |
+| [micode](https://github.com/vtemian/micode) | Lluvia de ideas estructurada → Planificar → Implementar flujo de trabajo con continuidad de sesión |
+| [octto](https://github.com/vtemian/octto) | Interfaz de usuario interactiva del navegador para lluvia de ideas de IA con formularios de preguntas múltiples |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agentes en segundo plano estilo Claude Code con delegación asíncrona y persistencia de contexto |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notificaciones nativas del sistema operativo para OpenCode: sepa cuándo se completan las tareas |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Arnés de orquestación multiagente incluido: 16 componentes, una instalación |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Árboles de trabajo de Git de fricción cero para OpenCode |
---
diff --git a/packages/web/src/content/docs/fr/ecosystem.mdx b/packages/web/src/content/docs/fr/ecosystem.mdx
index dd74e1f7b..46fcd7bde 100644
--- a/packages/web/src/content/docs/fr/ecosystem.mdx
+++ b/packages/web/src/content/docs/fr/ecosystem.mdx
@@ -6,71 +6,72 @@ description: Projets et intégrations construits avec OpenCode.
Une collection de projets communautaires construits sur OpenCode.
:::note
-Vous souhaitez ajouter votre projet lié à OpenCode à cette liste ? Soumettez un PR.
+Vous souhaitez ajouter votre projet lié à OpenCode à cette liste ? Soumettez une PR.
:::
-Vous pouvez également consulter [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) et [opencode.cafe](https://opencode.cafe), une communauté qui regroupe l'écosystème OpenCode.
+Vous pouvez également consulter [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) et [opencode.cafe](https://opencode.cafe), une communauté qui regroupe l'écosystème et la communauté.
---
## Extensions
-| Nom | Description |
-| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Exécute automatiquement des sessions OpenCode dans des environnements sandbox Daytona isolés avec synchronisation git et prévisualisations en direct |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Injecte automatiquement les en-têtes de session Helicone pour le regroupement des requêtes |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Injecte automatiquement les types TypeScript/Svelte dans les lectures de fichiers avec des outils de recherche |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Utilise votre abonnement ChatGPT Plus/Pro au lieu de crédits API |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Utilise votre forfait Gemini existant au lieu de la facturation API |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Utilise les modèles gratuits d'Antigravity au lieu de la facturation API |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Isolation de conteneur de développement multibranche avec clones superficiels et ports attribués automatiquement |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Plugin Google Antigravity OAuth, avec support de la recherche Google et gestion API plus robuste |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimise l'utilisation des jetons en éliminant les sorties d'outils obsolètes |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Ajoute le support natif de la recherche Web pour les fournisseurs pris en charge avec le style ancré par Google |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Permet aux agents IA d'exécuter des processus en arrière-plan dans un PTY et de leur envoyer des entrées interactives. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions pour les commandes shell non interactives - empêche les blocages des opérations dépendantes du TTY |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Suit l'utilisation de OpenCode avec Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Nettoie les tableaux Markdown produits par les LLM |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Édition de code 10 fois plus rapide avec Morph Fast Apply API et les marqueurs d'édition différée |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agents d'arrière-plan, outils LSP/AST/MCP prédéfinis, agents sélectionnés, compatibles Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notifications de bureau et alertes sonores pour les sessions OpenCode |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notifications sur le bureau et alertes sonores pour les événements d'autorisation, d'achèvement et d'erreur |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Dénomination automatique de session Zellij basée sur l'IA et le contexte OpenCode |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Autorise les agents OpenCode à charger paresseusement les prompts à la demande grâce à la découverte et à l'injection de compétences |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Mémoire persistante entre les sessions utilisant Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Révision interactive du plan avec annotation visuelle et partage privé/hors ligne |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Étend les commandes /commands d'opencode dans un système d'orchestration puissant avec contrôle de flux granulaire |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planifie des tâches récurrentes à l'aide de launchd (Mac) ou systemd (Linux) avec la syntaxe cron |
-| [micode](https://github.com/vtemian/micode) | Workflow structuré Brainstorming → Planifier → Mettre en œuvre avec continuité de session |
-| [octto](https://github.com/vtemian/octto) | Interface utilisateur de navigateur interactive pour le brainstorming IA avec des formulaires multi-questions |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agents d'arrière-plan de style Claude Code avec délégation asynchrone et persistance du contexte |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notifications natives du système d'exploitation pour OpenCode – savoir quand les tâches sont terminées |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Harness d'orchestration multi-agent prêt à l'emploi - 16 composants, une installation |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Arbres de travail Git sans friction pour OpenCode |
+| Nom | Description |
+| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Exécute automatiquement des sessions OpenCode dans des sandbox Daytona isolées avec synchronisation git et prévisualisations en direct |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Injecte automatiquement les en-têtes de session Helicone pour le regroupement des requêtes |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Injecte automatiquement les types TypeScript/Svelte dans les lectures de fichiers avec des outils de recherche |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Utilise votre abonnement ChatGPT Plus/Pro au lieu de crédits API |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Utilise votre forfait Gemini existant au lieu de la facturation API |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Utilise les modèles gratuits d'Antigravity au lieu de la facturation API |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Isolation de conteneur de développement multi-branches avec clones superficiels et ports attribués automatiquement |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Plugin OAuth Google Antigravity, avec prise en charge de la recherche Google et une gestion d'API plus robuste |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimise l'utilisation des jetons en élaguant les sorties d'outils obsolètes |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Masque les secrets/PII par des espaces réservés de style VibeGuard avant les appels LLM ; restaure localement |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Ajoute le support natif de la recherche web pour les fournisseurs pris en charge avec le style Google Grounding |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Permet aux agents IA d'exécuter des processus en arrière-plan dans un PTY et de leur envoyer des entrées interactives. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions pour les commandes shell non interactives - empêche les blocages dus aux opérations dépendantes du TTY |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Suit l'utilisation d'OpenCode avec Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Nettoie les tableaux Markdown produits par les LLM |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Édition de code 10x plus rapide avec l'API Morph Fast Apply et des marqueurs d'édition différée |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agents d'arrière-plan, outils LSP/AST/MCP pré-construits, agents sélectionnés, compatible Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notifications de bureau et alertes sonores pour les sessions OpenCode |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notifications de bureau et alertes sonores pour les événements de permission, d'achèvement et d'erreur |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Nommage automatique de session Zellij alimenté par l'IA basé sur le contexte OpenCode |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Permet aux agents OpenCode le chargement différé de prompts à la demande avec découverte et injection de compétences |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Mémoire persistante entre les sessions utilisant Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Révision de plan interactive avec annotation visuelle et partage privé/hors ligne |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Étend opencode /commands en un système d'orchestration puissant avec contrôle de flux granulaire |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planifie des tâches récurrentes en utilisant launchd (Mac) ou systemd (Linux) avec la syntaxe cron |
+| [micode](https://github.com/vtemian/micode) | Flux de travail structuré Brainstorming → Planification → Implémentation avec continuité de session |
+| [octto](https://github.com/vtemian/octto) | Interface utilisateur de navigateur interactive pour le brainstorming IA avec des formulaires multi-questions |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agents d'arrière-plan de style Claude Code avec délégation asynchrone et persistance du contexte |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notifications natives de l'OS pour OpenCode – sachez quand les tâches sont terminées |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Harnais d'orchestration multi-agents groupé – 16 composants, une installation |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Worktrees git sans friction pour OpenCode |
---
## Projets
-| Nom | Description |
-| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
-| [kimaki](https://github.com/remorses/kimaki) | Bot Discord pour contrôler les sessions OpenCode, construit sur le SDK |
-| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Plugin Neovim pour les prompts compatibles avec l'éditeur, construit sur l'API |
-| [portal](https://github.com/hosenur/portal) | Interface utilisateur Web axée sur le mobile pour OpenCode sur Tailscale/VPN |
-| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Modèle pour créer des plugins OpenCode |
-| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Frontend Neovim pour opencode - un agent de codage d'IA basé sur un terminal |
-| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Fournisseur Vercel AI SDK pour l'utilisation de OpenCode via @opencode-ai/sdk |
-| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Application Web/De bureau et extension VS Code pour OpenCode |
-| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Plugin Obsidian qui intègre OpenCode dans l'interface utilisateur d'Obsidian |
-| [OpenWork](https://github.com/different-ai/openwork) | Une alternative open source à Claude Cowork, propulsée par OpenCode |
-| [ocx](https://github.com/kdcokenny/ocx) | Gestionnaire d'extensions OpenCode avec profils portables et isolés. |
-| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Application client de bureau, Web, mobile et distante pour OpenCode |
+| Nom | Description |
+| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
+| [kimaki](https://github.com/remorses/kimaki) | Bot Discord pour contrôler les sessions OpenCode, construit sur le SDK |
+| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Plugin Neovim pour des prompts conscients de l'éditeur, construit sur l'API |
+| [portal](https://github.com/hosenur/portal) | Interface Web mobile-first pour OpenCode via Tailscale/VPN |
+| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Modèle pour créer des plugins OpenCode |
+| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Frontend Neovim pour opencode - un agent de codage IA basé sur terminal |
+| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Fournisseur Vercel AI SDK pour utiliser OpenCode via @opencode-ai/sdk |
+| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Application Web / Bureau et extension VS Code pour OpenCode |
+| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Plugin Obsidian qui intègre OpenCode dans l'interface d'Obsidian |
+| [OpenWork](https://github.com/different-ai/openwork) | Une alternative open-source à Claude Cowork, propulsée par OpenCode |
+| [ocx](https://github.com/kdcokenny/ocx) | Gestionnaire d'extensions OpenCode avec profils portables et isolés. |
+| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Application client Bureau, Web, Mobile et Distante pour OpenCode |
---
## Agents
-| Nom | Description |
-| ----------------------------------------------------------------- | ----------------------------------------------------------------------------- |
-| [Agentic](https://github.com/Cluster444/agentic) | Agents et commandes d'IA modulaires pour un développement structuré |
-| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | Configurations, prompts, agents et plugins pour des flux de travail améliorés |
+| Nom | Description |
+| ----------------------------------------------------------------- | ---------------------------------------------------------------------- |
+| [Agentic](https://github.com/Cluster444/agentic) | Agents IA modulaires et commandes pour un développement structuré |
+| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | Configs, prompts, agents et plugins pour des flux de travail améliorés |
diff --git a/packages/web/src/content/docs/go.mdx b/packages/web/src/content/docs/go.mdx
index 8af01b731..5976f1a35 100644
--- a/packages/web/src/content/docs/go.mdx
+++ b/packages/web/src/content/docs/go.mdx
@@ -85,9 +85,9 @@ The table below provides an estimated request count based on typical Go usage pa
| | GLM-5 | Kimi K2.5 | MiniMax M2.5 |
| ------------------- | ----- | --------- | ------------ |
-| requests per 5 hour | 1,150 | 1,850 | 30,000 |
-| requests per week | 2,880 | 4,630 | 75,000 |
-| requests per month | 5,750 | 9,250 | 150,000 |
+| requests per 5 hour | 1,150 | 1,850 | 20,000 |
+| requests per week | 2,880 | 4,630 | 50,000 |
+| requests per month | 5,750 | 9,250 | 100,000 |
Estimates are based on observed average request patterns:
@@ -105,18 +105,6 @@ Usage limits may change as we learn from early usage and feedback.
---
-### Pricing
-
-OpenCode Go is a **$10/month** subscription plan. Below are the prices **per 1M tokens**.
-
-| Model | Input | Output | Cached Read |
-| ------------ | ----- | ------ | ----------- |
-| GLM-5 | $1.00 | $3.20 | $0.20 |
-| Kimi K2.5 | $0.60 | $3.00 | $0.10 |
-| MiniMax M2.5 | $0.30 | $1.20 | $0.03 |
-
----
-
### Usage beyond limits
If you also have credits on your Zen balance, you can enable the **Use balance**
diff --git a/packages/web/src/content/docs/it/ecosystem.mdx b/packages/web/src/content/docs/it/ecosystem.mdx
index 54fcdb8db..11ce7193f 100644
--- a/packages/web/src/content/docs/it/ecosystem.mdx
+++ b/packages/web/src/content/docs/it/ecosystem.mdx
@@ -3,50 +3,51 @@ title: Ecosistema
description: Progetti e integrazioni costruiti con OpenCode.
---
-Una raccolta di progetti della comunita costruiti su OpenCode.
+Una raccolta di progetti della comunità costruiti su OpenCode.
:::note
Vuoi aggiungere il tuo progetto legato a OpenCode a questa lista? Apri una PR.
:::
-Puoi anche dare un'occhiata a [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) e [opencode.cafe](https://opencode.cafe), una comunita che aggrega ecosistema e community.
+Puoi anche dare un'occhiata a [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) e [opencode.cafe](https://opencode.cafe), una comunità che aggrega ecosistema e community.
---
## Plugin
-| Nome | Descrizione |
-| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Esegue automaticamente sessioni OpenCode in sandbox Daytona isolate con sync git e anteprime live |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Inietta automaticamente gli header di sessione Helicone per raggruppare le richieste |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Inietta automaticamente tipi TypeScript/Svelte nelle letture dei file con tool di lookup |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Usa il tuo abbonamento ChatGPT Plus/Pro invece dei crediti API |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Usa il tuo piano Gemini esistente invece della fatturazione API |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Usa i modelli gratuiti di Antigravity invece della fatturazione API |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Isolamento devcontainer multi-branch con shallow clone e porte assegnate automaticamente |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Plugin OAuth Google Antigravity, con supporto a Google Search e gestione API piu robusta |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Ottimizza l'uso dei token eliminando output obsoleti degli strumenti |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Aggiunge supporto websearch nativo per provider supportati con stile grounded di Google |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Permette agli agenti AI di eseguire processi in background in una PTY e inviare input interattivo |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Istruzioni per comandi shell non interattivi: evita blocchi dovuti a operazioni dipendenti da TTY |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Traccia l'uso di OpenCode con Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Ripulisce le tabelle markdown prodotte dai LLM |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Editing del codice 10x piu veloce con Morph Fast Apply API e marker lazy edit |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agenti in background, tool LSP/AST/MCP predefiniti, agenti curati, compatibile con Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notifiche desktop e avvisi sonori per le sessioni OpenCode |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notifiche desktop e avvisi sonori per eventi di permesso, completamento ed errore |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Naming automatico delle sessioni Zellij basato sul contesto OpenCode |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Permette agli agenti OpenCode di caricare prompt al bisogno con discovery e injection di skill |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Memoria persistente tra sessioni usando Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Revisione interattiva dei piani con annotazione visiva e condivisione privata/offline |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Estende opencode /commands in un sistema di orchestrazione con controllo di flusso granulare |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Pianifica job ricorrenti con launchd (Mac) o systemd (Linux) usando sintassi cron |
-| [micode](https://github.com/vtemian/micode) | Workflow strutturato Brainstorm → Plan → Implement con continuita di sessione |
-| [octto](https://github.com/vtemian/octto) | UI browser interattiva per brainstorming AI con moduli multi-domanda |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agenti in background stile Claude Code con delega async e persistenza del contesto |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notifiche native del sistema per OpenCode: sai quando i task finiscono |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Harness di orchestrazione multi-agente bundle: 16 componenti, una installazione |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Git worktree senza attriti per OpenCode |
+| Nome | Descrizione |
+| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Esegue automaticamente sessioni OpenCode in sandbox Daytona isolate con sync git e anteprime live |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Inietta automaticamente gli header di sessione Helicone per raggruppare le richieste |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Inietta automaticamente tipi TypeScript/Svelte nelle letture dei file con tool di lookup |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Usa il tuo abbonamento ChatGPT Plus/Pro invece dei crediti API |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Usa il tuo piano Gemini esistente invece della fatturazione API |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Usa i modelli gratuiti di Antigravity invece della fatturazione API |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Isolamento devcontainer multi-branch con shallow clone e porte assegnate automaticamente |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Plugin OAuth Google Antigravity, con supporto a Google Search e gestione API più robusta |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Ottimizza l'uso dei token eliminando output obsoleti degli strumenti |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Oscura segreti/PII in placeholder stile VibeGuard prima delle chiamate LLM; ripristina localmente |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Aggiunge supporto websearch nativo per provider supportati con stile grounded di Google |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Permette agli agenti AI di eseguire processi in background in una PTY e inviare input interattivo |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Istruzioni per comandi shell non interattivi: evita blocchi dovuti a operazioni dipendenti da TTY |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Traccia l'uso di OpenCode con Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Ripulisce le tabelle markdown prodotte dai LLM |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Editing del codice 10x più veloce con Morph Fast Apply API e marker lazy edit |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agenti in background, tool LSP/AST/MCP predefiniti, agenti curati, compatibile con Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notifiche desktop e avvisi sonori per le sessioni OpenCode |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notifiche desktop e avvisi sonori per eventi di permesso, completamento ed errore |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Naming automatico delle sessioni Zellij basato sul contesto OpenCode |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Permette agli agenti OpenCode di caricare prompt al bisogno con discovery e injection di skill |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Memoria persistente tra sessioni usando Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Revisione interattiva dei piani con annotazione visiva e condivisione privata/offline |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Estende opencode /commands in un sistema di orchestrazione con controllo di flusso granulare |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Pianifica job ricorrenti con launchd (Mac) o systemd (Linux) usando sintassi cron |
+| [micode](https://github.com/vtemian/micode) | Workflow strutturato Brainstorm → Plan → Implement con continuità di sessione |
+| [octto](https://github.com/vtemian/octto) | UI browser interattiva per brainstorming AI con moduli multi-domanda |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agenti in background stile Claude Code con delega async e persistenza del contesto |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notifiche native del sistema per OpenCode: sai quando i task finiscono |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Harness di orchestrazione multi-agente bundle: 16 componenti, una installazione |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Git worktree senza attriti per OpenCode |
---
diff --git a/packages/web/src/content/docs/ja/ecosystem.mdx b/packages/web/src/content/docs/ja/ecosystem.mdx
index 479fdfbdc..711b31d48 100644
--- a/packages/web/src/content/docs/ja/ecosystem.mdx
+++ b/packages/web/src/content/docs/ja/ecosystem.mdx
@@ -8,44 +8,46 @@ OpenCode に基づいて構築されたコミュニティプロジェクトの�
:::note
OpenCode 関連プロジェクトをこのリストに追加したいですか? PR を送信してください。
:::
+
[awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) および [opencode.cafe](https://opencode.cafe) もチェックしてください。
---
## プラグイン
-| 名前 | 説明 |
-| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | git sync とライブプレビューを使用して、隔離された Daytona サンドボックスで OpenCode セッションを自動的に実行します。 |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | リクエストのグループ化のために Helicone セッションヘッダーを自動的に挿入する |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | ルックアップツールを使用して TypeScript/Svelte 型をファイル読み取りに自動挿入する |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | API クレジットの代わりに ChatGPT Plus/Pro サブスクリプションを使用する |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | API 課金の代わりに既存の Gemini プランを使用する |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | API 課金の代わりに Antigravity の無料モデルを使用する |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | 浅いクローンと自動割り当てポートを使用したマルチブランチ devcontainer の分離 |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth プラグイン、Google 検索のサポート、およびより堅牢な API 処理 |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 古いツールの出力を削除してトークンの使用を最適化する |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Google ベースのスタイルでサポートされているプロバイダーにネイティブ Web 検索サポートを追加 |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | AI エージェントが PTY でバックグラウンドプロセスを実行し、インタラクティブな入力を送信できるようにします。 |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 非対話型シェルコマンドの手順 - TTY に依存する操作によるハングの防止 |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | wakatime で OpenCode の使用状況を追跡する |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLM によって生成された Markdown テーブルをクリーンアップする |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast apply API と遅延編集マーカーにより 10 倍高速なコード編集 |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | バックグラウンドエージェント、事前構築された LSP/AST/MCP ツール、厳選されたエージェント、Claude Code 互換 |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode セッションのデスクトップ通知とサウンドアラート |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 許可、完了、エラーイベントのデスクトップ通知とサウンドアラート |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | OpenCode コンテキストに基づいた AI による自動 Zellij セッション命名 |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | OpenCode エージェントがスキルの検出と挿入を使用してオンデマンドでプロンプトを遅延ロードできるようにする |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | スーパーメモリを使用したセッション間での永続メモリ |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | 視覚的な注釈とプライベート/オフライン共有による対話型の計画レビュー |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | OpenCode/コマンドをきめ細かいフロー制御を備えた強力なオーケストレーションシステムに拡張 |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | launchd (Mac) または systemd (Linux) を cron 構文で使用して、定期的なジョブをスケジュールする |
-| [micode](https://github.com/vtemian/micode) | 構造化されたブレインストーミング → 計画 → セッション継続性のあるワークフローの実装 |
-| [octto](https://github.com/vtemian/octto) | 複数の質問フォームを使用した AI ブレインストーミング用のインタラクティブなブラウザ UI |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | 非同期委任とコンテキスト永続性を備えた Claude Code スタイルのバックグラウンドエージェント |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode のネイティブ OS 通知 – タスクがいつ完了したかを知る |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | バンドルされたマルチエージェントオーケストレーションハーネス – 16 コンポーネント、1 回のインストール |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode 用のゼロフリクション Git ワークツリー |
+| 名前 | 説明 |
+| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | git sync とライブプレビューを使用して、隔離された Daytona サンドボックスで OpenCode セッションを自動的に実行します。 |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | リクエストのグループ化のために Helicone セッションヘッダーを自動的に挿入する |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | ルックアップツールを使用して TypeScript/Svelte 型をファイル読み取りに自動挿入する |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | API クレジットの代わりに ChatGPT Plus/Pro サブスクリプションを使用する |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | API 課金の代わりに既存の Gemini プランを使用する |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | API 課金の代わりに Antigravity の無料モデルを使用する |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | 浅いクローンと自動割り当てポートを使用したマルチブランチ devcontainer の分離 |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth プラグイン、Google 検索のサポート、およびより堅牢な API 処理 |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 古いツールの出力を削除してトークンの使用を最適化する |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | LLM 呼び出しの前にシークレット/PII を VibeGuard スタイルのプレースホルダーに編集し、ローカルで復元する |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Google ベースのスタイルでサポートされているプロバイダーにネイティブ Web 検索サポートを追加 |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | AI エージェントが PTY でバックグラウンドプロセスを実行し、インタラクティブな入力を送信できるようにします。 |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 非対話型シェルコマンドの手順 - TTY に依存する操作によるハングの防止 |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | wakatime で OpenCode の使用状況を追跡する |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLM によって生成された Markdown テーブルをクリーンアップする |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast apply API と遅延編集マーカーにより 10 倍高速なコード編集 |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | バックグラウンドエージェント、事前構築された LSP/AST/MCP ツール、厳選されたエージェント、Claude Code 互換 |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode セッションのデスクトップ通知とサウンドアラート |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 許可、完了、エラーイベントのデスクトップ通知とサウンドアラート |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | OpenCode コンテキストに基づいた AI による自動 Zellij セッション命名 |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | OpenCode エージェントがスキルの検出と挿入を使用してオンデマンドでプロンプトを遅延ロードできるようにする |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | スーパーメモリを使用したセッション間での永続メモリ |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | 視覚的な注釈とプライベート/オフライン共有による対話型の計画レビュー |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | OpenCode/コマンドをきめ細かいフロー制御を備えた強力なオーケストレーションシステムに拡張 |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | launchd (Mac) または systemd (Linux) を cron 構文で使用して、定期的なジョブをスケジュールする |
+| [micode](https://github.com/vtemian/micode) | 構造化されたブレインストーミング → 計画 → セッション継続性のあるワークフローの実装 |
+| [octto](https://github.com/vtemian/octto) | 複数の質問フォームを使用した AI ブレインストーミング用のインタラクティブなブラウザ UI |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | 非同期委任とコンテキスト永続性を備えた Claude Code スタイルのバックグラウンドエージェント |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode のネイティブ OS 通知 – タスクがいつ完了したかを知る |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | バンドルされたマルチエージェントオーケストレーションハーネス – 16 コンポーネント、1 回のインストール |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode 用のゼロフリクション Git ワークツリー |
---
diff --git a/packages/web/src/content/docs/ko/ecosystem.mdx b/packages/web/src/content/docs/ko/ecosystem.mdx
index 9f6a8f9bc..c0f542da9 100644
--- a/packages/web/src/content/docs/ko/ecosystem.mdx
+++ b/packages/web/src/content/docs/ko/ecosystem.mdx
@@ -15,38 +15,39 @@ OpenCode를 기반으로 만들어진 커뮤니티 프로젝트 모음입니다.
## 플러그인
-| 이름 | 설명 |
-| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | git sync와 live preview를 지원하는 격리된 Daytona sandbox에서 OpenCode 세션을 자동 실행합니다. |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | 요청을 그룹화할 수 있도록 Helicone session header를 자동으로 주입합니다. |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | 조회 tool과 함께 TypeScript/Svelte 타입 정보를 파일 읽기에 자동 주입합니다. |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | API 크레딧 대신 ChatGPT Plus/Pro 구독을 사용할 수 있습니다. |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | API 과금 대신 기존 Gemini 플랜을 사용할 수 있습니다. |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | API 과금 대신 Antigravity의 무료 model을 사용할 수 있습니다. |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | shallow clone과 자동 포트 할당을 기반으로 multi-branch devcontainer 격리를 제공합니다. |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Search 지원과 견고한 API 처리를 제공하는 Google Antigravity OAuth Plugin입니다. |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 오래된 tool output을 정리해 token 사용량을 최적화합니다. |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | 지원 provider에서 Google grounded 스타일의 네이티브 websearch를 추가합니다. |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | AI agent가 PTY에서 백그라운드 프로세스를 실행하고 대화형 입력을 보낼 수 있게 합니다. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 비대화형 shell 명령 실행 지침을 제공해 TTY 의존 작업으로 인한 멈춤을 방지합니다. |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Wakatime으로 OpenCode 사용량을 추적합니다. |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLM이 생성한 markdown 표를 정리합니다. |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast Apply API와 lazy edit marker를 활용해 코드 편집 속도를 크게 높입니다. |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | background agent, 사전 구성된 LSP/AST/MCP tool, curated agent, Claude Code 호환성을 제공합니다. |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 세션에 데스크톱 알림과 사운드 알림을 제공합니다. |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | permission, 완료, 오류 이벤트에 대한 데스크톱 알림과 사운드 알림을 제공합니다. |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | OpenCode 맥락을 기반으로 Zellij session 이름을 AI로 자동 지정합니다. |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | skill 탐색과 주입을 통해 OpenCode agent가 필요 시 prompt를 lazy load하도록 합니다. |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Supermemory를 사용해 세션 간 persistent memory를 제공합니다. |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | 시각 주석과 private/offline 공유를 포함한 인터랙티브 계획 리뷰를 제공합니다. |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | 세밀한 flow control로 opencode /commands를 강력한 orchestration 시스템으로 확장합니다. |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | cron 문법을 사용해 launchd(Mac) 또는 systemd(Linux) 기반 반복 작업을 예약합니다. |
-| [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → Implement 워크플로를 session continuity와 함께 제공합니다. |
-| [octto](https://github.com/vtemian/octto) | 다중 질문 폼 기반의 AI 브레인스토밍용 인터랙티브 브라우저 UI를 제공합니다. |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code 스타일의 background agent를 async delegation과 context persistence로 제공합니다. |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode 작업 완료 시점을 native OS 알림으로 알려줍니다. |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | 16개 구성요소를 한 번에 설치하는 bundled multi-agent orchestration harness를 제공합니다. |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode용 git worktree를 손쉽게 사용할 수 있도록 돕습니다. |
+| 이름 | 설명 |
+| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | git sync와 live preview를 지원하는 격리된 Daytona sandbox에서 OpenCode 세션을 자동 실행합니다. |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | 요청을 그룹화할 수 있도록 Helicone session header를 자동으로 주입합니다. |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | 조회 tool과 함께 TypeScript/Svelte 타입 정보를 파일 읽기에 자동 주입합니다. |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | API 크레딧 대신 ChatGPT Plus/Pro 구독을 사용할 수 있습니다. |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | API 과금 대신 기존 Gemini 플랜을 사용할 수 있습니다. |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | API 과금 대신 Antigravity의 무료 model을 사용할 수 있습니다. |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | shallow clone과 자동 포트 할당을 기반으로 multi-branch devcontainer 격리를 제공합니다. |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Search 지원과 견고한 API 처리를 제공하는 Google Antigravity OAuth Plugin입니다. |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 오래된 tool output을 정리해 token 사용량을 최적화합니다. |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | LLM 호출 전에 secrets/PII를 VibeGuard 스타일 placeholder로 가리고, 로컬에서 복원합니다. |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | 지원 provider에서 Google grounded 스타일의 네이티브 websearch를 추가합니다. |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | AI agent가 PTY에서 백그라운드 프로세스를 실행하고 대화형 입력을 보낼 수 있게 합니다. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 비대화형 shell 명령 실행 지침을 제공해 TTY 의존 작업으로 인한 멈춤을 방지합니다. |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Wakatime으로 OpenCode 사용량을 추적합니다. |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLM이 생성한 markdown 표를 정리합니다. |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast Apply API와 lazy edit marker를 활용해 코드 편집 속도를 크게 높입니다. |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | background agent, 사전 구성된 LSP/AST/MCP tool, curated agent, Claude Code 호환성을 제공합니다. |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 세션에 데스크톱 알림과 사운드 알림을 제공합니다. |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | permission, 완료, 오류 이벤트에 대한 데스크톱 알림과 사운드 알림을 제공합니다. |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | OpenCode 맥락을 기반으로 Zellij session 이름을 AI로 자동 지정합니다. |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | skill 탐색과 주입을 통해 OpenCode agent가 필요 시 prompt를 lazy load하도록 합니다. |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Supermemory를 사용해 세션 간 persistent memory를 제공합니다. |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | 시각 주석과 private/offline 공유를 포함한 인터랙티브 계획 리뷰를 제공합니다. |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | 세밀한 flow control로 opencode /commands를 강력한 orchestration 시스템으로 확장합니다. |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | cron 문법을 사용해 launchd(Mac) 또는 systemd(Linux) 기반 반복 작업을 예약합니다. |
+| [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → Implement 워크플로를 session continuity와 함께 제공합니다. |
+| [octto](https://github.com/vtemian/octto) | 다중 질문 폼 기반의 AI 브레인스토밍용 인터랙티브 브라우저 UI를 제공합니다. |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code 스타일의 background agent를 async delegation과 context persistence로 제공합니다. |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode 작업 완료 시점을 native OS 알림으로 알려줍니다. |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | 16개 구성요소를 한 번에 설치하는 bundled multi-agent orchestration harness를 제공합니다. |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode용 git worktree를 손쉽게 사용할 수 있도록 돕습니다. |
---
diff --git a/packages/web/src/content/docs/nb/ecosystem.mdx b/packages/web/src/content/docs/nb/ecosystem.mdx
index 714a9ee95..bc4a57737 100644
--- a/packages/web/src/content/docs/nb/ecosystem.mdx
+++ b/packages/web/src/content/docs/nb/ecosystem.mdx
@@ -15,38 +15,39 @@ Du kan også sjekke ut [awesome-opencode](https://github.com/awesome-opencode/aw
## Utvidelser
-| Navn | Beskrivelse |
-| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Kjør OpenCode-økter automatisk i isolerte Daytona-sandkasser med git-synkronisering og live-forhåndsvisninger |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Injiser automatisk Helicone-headers for forespørselsgruppering |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-injiser TypeScript/Svelte-typer i fillesninger med oppslagsverktøy |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Bruk ChatGPT Plus/Pro-abonnementet ditt i stedet for API kreditter |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Bruk din eksisterende Gemini-plan i stedet for API-fakturering |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Bruk Antigravitys gratis modeller i stedet for API-fakturering |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-branch devcontainer-isolasjon med grunne kloner og automatisk tildelte porter |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth-plugin, med støtte for Google Søk og mer robust API-håndtering |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimaliser bruken av token ved å beskjære utdaterte verktøy |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Legg til innebygd støtte for nettsøk for støttede leverandører med Googles kildebaserte stil |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Gjør det mulig for AI-agenter å kjøre bakgrunnsprosesser i en PTY, sende interaktiv inndata til dem. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instruksjoner for ikke-interaktive skallkommandoer - forhindrer heng ved TTY-avhengige operasjoner |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Spor OpenCode-bruk med Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Rydd opp i markdown-tabeller produsert av LLMs |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10 ganger raskere koderedigering med Morph Fast Apply API og lazy-redigeringsmarkører |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Bakgrunnsagenter, forhåndsbygde LSP/AST/MCP verktøy, kurerte agenter, Claude Code-kompatibel |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Skrivebordsvarsler og lydvarsler for OpenCode-økter |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Skrivebordsvarsler og lydvarsler for tillatelse, fullføring og feilhendelser |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-drevet automatisk Zellij-sesjonsnavn basert på OpenCode-kontekst |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Tillat OpenCode-agenter å lazy-loade meldinger på forespørsel med ferdighetsoppdagelse og injeksjon |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Vedvarende minne på tvers av økter ved hjelp av Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktiv plangjennomgang med visuell merknad og privat/offline deling |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Utvid OpenCode /kommandoer til et kraftig orkestreringssystem med granulær flytkontroll |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planlegg gjentakende jobber ved hjelp av launchd (Mac) eller systemd (Linux) med cron-syntaks |
-| [micode](https://github.com/vtemian/micode) | Strukturert brainstorm → Plan → Implementer arbeidsflyt med øktkontinuitet |
-| [octto](https://github.com/vtemian/octto) | Interaktiv nettleser UI for AI idédugnad med flerspørsmålsskjemaer |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Bakgrunnsagenter i kodestil med asynkrondelegering og kontekstutholdenhet |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Innfødte OS-varsler for OpenCode – vet når oppgaver fullføres |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Medfølgende multi-agent orkestreringsrammeverk – 16 komponenter, én installasjon |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Nullfriksjon git-arbeidstre for OpenCode |
+| Navn | Beskrivelse |
+| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Kjør OpenCode-økter automatisk i isolerte Daytona-sandkasser med git-synkronisering og live-forhåndsvisninger |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Injiser automatisk Helicone-headers for forespørselsgruppering |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-injiser TypeScript/Svelte-typer i fillesninger med oppslagsverktøy |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Bruk ChatGPT Plus/Pro-abonnementet ditt i stedet for API kreditter |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Bruk din eksisterende Gemini-plan i stedet for API-fakturering |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Bruk Antigravitys gratis modeller i stedet for API-fakturering |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Multi-branch devcontainer-isolasjon med grunne kloner og automatisk tildelte porter |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth-plugin, med støtte for Google Søk og mer robust API-håndtering |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimaliser bruken av token ved å beskjære utdaterte verktøy |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Sladd hemmeligheter/PII til VibeGuard-stil plassholdere før LLM-kall; gjenopprett lokalt |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Legg til innebygd støtte for nettsøk for støttede leverandører med Googles kildebaserte stil |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Gjør det mulig for AI-agenter å kjøre bakgrunnsprosesser i en PTY, sende interaktiv inndata til dem. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instruksjoner for ikke-interaktive skallkommandoer - forhindrer heng ved TTY-avhengige operasjoner |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Spor OpenCode-bruk med Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Rydd opp i markdown-tabeller produsert av LLMs |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10 ganger raskere koderedigering med Morph Fast Apply API og lazy-redigeringsmarkører |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Bakgrunnsagenter, forhåndsbygde LSP/AST/MCP verktøy, kurerte agenter, Claude Code-kompatibel |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Skrivebordsvarsler og lydvarsler for OpenCode-økter |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Skrivebordsvarsler og lydvarsler for tillatelse, fullføring og feilhendelser |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-drevet automatisk Zellij-sesjonsnavn basert på OpenCode-kontekst |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Tillat OpenCode-agenter å lazy-loade meldinger på forespørsel med ferdighetsoppdagelse og injeksjon |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Vedvarende minne på tvers av økter ved hjelp av Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktiv plangjennomgang med visuell merknad og privat/offline deling |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Utvid OpenCode /kommandoer til et kraftig orkestreringssystem med granulær flytkontroll |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planlegg gjentakende jobber ved hjelp av launchd (Mac) eller systemd (Linux) med cron-syntaks |
+| [micode](https://github.com/vtemian/micode) | Strukturert brainstorm → Plan → Implementer arbeidsflyt med øktkontinuitet |
+| [octto](https://github.com/vtemian/octto) | Interaktiv nettleser UI for AI idédugnad med flerspørsmålsskjemaer |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code-stil bakgrunnsagenter med asynkrondelegering og kontekstbevaring |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Innfødte OS-varsler for OpenCode – vet når oppgaver fullføres |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Medfølgende multi-agent orkestreringsrammeverk – 16 komponenter, én installasjon |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Nullfriksjon git-arbeidstre for OpenCode |
---
diff --git a/packages/web/src/content/docs/pl/ecosystem.mdx b/packages/web/src/content/docs/pl/ecosystem.mdx
index 7c75340c5..7ae5cce3e 100644
--- a/packages/web/src/content/docs/pl/ecosystem.mdx
+++ b/packages/web/src/content/docs/pl/ecosystem.mdx
@@ -1,76 +1,77 @@
---
title: Ekosystem
-description: Projekty i integracje zbudowane w opencode.
+description: Projekty i integracje zbudowane przy użyciu OpenCode.
---
-Zgromadzenie stowarzyszenia organizacji na opencode.
+Zbiór projektów społeczności zbudowanych na OpenCode.
:::note
-Chcesz zadać swój projekt badawczy z opencode do tej listy? Prześlij PR.
+Chcesz dodać swój projekt związany z OpenCode do tej listy? Prześlij PR.
:::
-Możesz także sprawdzić [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) i [opencode.cafe](https://opencode.cafe), grupę skupiającą ekosystem i społeczność.
+Możesz również sprawdzić [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) oraz [opencode.cafe](https://opencode.cafe), społeczność agregującą ekosystem i społeczność.
---
-## Wtyki
+## Wtyczki
-| Imię | Opis |
-| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Automatycznie uruchamiaj sesje opencode w izolowanych piaskownicach Daytona z synchronizacją git i podglądami na żywo |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatycznie wstawiaj nagłówki sesji Helicone w celu grupowania urządzeń |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Automatyczne wstrzykiwacze TypeScript/Svelte do odczytania plików za pomocą narzędzi wyszukiwania |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | wykorzystać do wykorzystania ChatGPT Plus/Pro zamiast kredytu API |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | korzystać z planu Gemini zamiast rozliczeń API |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Wykorzystanie z bezpłatnych modeli Antigravity zamiast rozliczeń API |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Izolacja wielooddziałowych kontenerów deweloperskich z płytkami klonami i automatycznie przypisywanymi portami |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Wtyczka Google Antigravity OAuth z obsługą obsługi Google i bardziej niezawodną obsługą API |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Zoptymalizuj wykorzystanie tokena, usuwając przestarzałe dane wyjściowe narzędzia |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Dodaj natywną obsługę wyszukiwania w sieci dla dostawców w stylu opartym na Google |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Uruchomienie agenta AI uruchamiającego się w tle w PTY i wytwarzanie ich interaktywnych danych. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instrukcje dla nieinteraktywnych obowiązków - zaniechanie zawieszenia operacji zależnych od TTY |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Śledź udostępnić opencode za pomocą Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Oczyść tabelę przecenioną przez LLM |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x szybsza edycja kodu dzięki Morph Fast Apply API i znacznikom leniwej edycji |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agencje odpowiedzialne w tle, gotowe narzędzia LSP/AST/MCP, wyselekcjonowani agenci, kompatybilni z Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Powiadomienia na pulpicie i alerty dźwiękowe dotyczące sesji opencode |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Powiadomienia na pulpicie i alerty dźwiękowe dotyczące uprawnień, wyników i zdarzeń o błędach |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Automatyczne nazewnictwo sesji Zellij oparte na sztucznej inteligencji w oparciu o kontekst opencode |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Zezwalaj agentom opencode na leniwe ładowanie podpowiedzi na podstawie odkrywania możliwości i wstrzykiwania |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Trwała pamięć w sesjach przy użyciu Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktywny przegląd planu z adnotacją wizualną i użytkową prywatną/offline |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Rozszerzony opencode/polecenia do połączenia sieciowego ze szczegółową kontrolą bezpieczeństwa |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Zaplanuj powtarzające się zadania, używając launchd (Mac) lub systemd (Linux) ze składaną cron |
-| [micode](https://github.com/vtemian/micode) | Ustrukturyzowana burza mózgów → Plan → Wdrożenie wyjścia z ciągłością sesji |
-| [octto](https://github.com/vtemian/octto) | Interaktywny interfejs do burzy mózgów AI z formularzami kontrolnymi wielu pytań |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agencje krytyczne w tle w stylu Claude Code z delegowaniem asynchronicznym i trwałością kontekstu |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Natywne uruchomienie systemu dla opencode – wiesz, kiedy zadania zostaną zakończone |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Lista wiązek orkiestracji wieloagentowej – 16 dostępna, jedna instalacja |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Drzewa robocze Git o zerowym tarciu dla opencode |
+| Nazwa | Opis |
+| -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Automatyczne uruchamianie sesji OpenCode w izolowanych piaskownicach Daytona z synchronizacją git i podglądem na żywo |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatyczne wstrzykiwanie nagłówków sesji Helicone do grupowania żądań |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Automatyczne wstrzykiwanie typów TypeScript/Svelte do odczytów plików za pomocą narzędzi wyszukiwania |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Użyj subskrypcji ChatGPT Plus/Pro zamiast kredytów API |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Użyj istniejącego planu Gemini zamiast płatności za API |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Użyj darmowych modeli Antigravity zamiast płatności za API |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Izolacja kontenerów deweloperskich dla wielu gałęzi z płytkim klonowaniem i automatycznie przypisywanymi portami |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Wtyczka Google Antigravity OAuth ze wsparciem dla wyszukiwarki Google i bardziej niezawodną obsługą API |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optymalizacja zużycia tokenów poprzez usuwanie przestarzałych wyników narzędzi |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Redagowanie sekretów/danych osobowych do placeholderów w stylu VibeGuard przed wywołaniem LLM; przywracanie lokalnie |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Dodaj natywną obsługę wyszukiwania w sieci dla wspieranych dostawców w stylu Google grounded |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Umożliwia agentom AI uruchamianie procesów w tle w PTY i wysyłanie do nich interaktywnych danych wejściowych. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instrukcje dla nieinteraktywnych poleceń powłoki - zapobiega zawieszeniom operacji zależnych od TTY |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Śledź użycie OpenCode za pomocą Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Oczyść tabele markdown generowane przez LLM |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x szybsza edycja kodu dzięki API Morph Fast Apply i leniwym znacznikom edycji |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agenci w tle, wbudowane narzędzia LSP/AST/MCP, wyselekcjonowani agenci, kompatybilność z Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Powiadomienia na pulpicie i alerty dźwiękowe dla sesji OpenCode |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Powiadomienia na pulpicie i alerty dźwiękowe dla uprawnień, zakończeń zadań i błędów |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Automatyczne nazywanie sesji Zellij oparte na AI w oparciu o kontekst OpenCode |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Pozwól agentom OpenCode na leniwe ładowanie promptów na żądanie z odkrywaniem umiejętności i wstrzykiwaniem |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Trwała pamięć między sesjami przy użyciu Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktywny przegląd planów z wizualnymi adnotacjami i prywatnym/offline udostępnianiem |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Rozszerz komendy /commands OpenCode w potężny system orkiestracji ze szczegółową kontrolą przepływu |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planuj cykliczne zadania używając launchd (Mac) lub systemd (Linux) ze składnią cron |
+| [micode](https://github.com/vtemian/micode) | Ustrukturyzowany przepływ pracy Burza mózgów → Plan → Implementacja z ciągłością sesji |
+| [octto](https://github.com/vtemian/octto) | Interaktywny interfejs przeglądarkowy do burzy mózgów AI z formularzami wielopytaniowymi |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agenci w tle w stylu Claude Code z asynchronicznym delegowaniem i trwałością kontekstu |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Natywne powiadomienia systemowe dla OpenCode – wiedz, kiedy zadania się zakończą |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Zestaw orkiestracji wieloagentowej – 16 komponentów, jedna instalacja |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Bezproblemowe drzewa robocze git dla OpenCode |
---
-## Projektowanie
+## Projekty
-| Imię | Opis |
-| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
-| [kimaki](https://github.com/remorses/kimaki) | Bot Discord do kontrolowania sesji opencode, zbudowany na SDK |
-| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Wtyczka Neovim do podpowiedzi, zbudowana w oparciu o API |
-| [portal](https://github.com/hosenur/portal) | Interfejs sieciowy do urządzeń mobilnych dla opencode poprzez Tailscale/VPN |
-| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Szablon do budowy wtyczek opencode |
-| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Frontend Neovim dla opencode - agent kodujący AI oparty na terminalu |
-| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Stosowanie Vercel AI SDK do użytku z opencode poprzez @opencode-ai/sdk |
-| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Aplikacja internetowa/stacjonarna i rozszerzenie VS Code dla opencode |
-| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Wtyczka Obsidian osadzająca opencode w interfejsie użytkownika Obsidian |
-| [OpenWork](https://github.com/different-ai/openwork) | Alternatywa typu open source dla Claude Cowork, obsługa przez opencode |
-| [ocx](https://github.com/kdcokenny/ocx) | Menedżer rozszerzony opencode z przenośnymi, izolowanymi profilami. |
-| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Aplikacja komputerowa, internetowa, mobilna i zdalna dla opencode |
+| Nazwa | Opis |
+| ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
+| [kimaki](https://github.com/remorses/kimaki) | Bot Discord do sterowania sesjami OpenCode, zbudowany na SDK |
+| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Wtyczka Neovim dla promptów świadomych edytora, zbudowana na API |
+| [portal](https://github.com/hosenur/portal) | Interfejs webowy mobile-first dla OpenCode przez Tailscale/VPN |
+| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Szablon do tworzenia wtyczek OpenCode |
+| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Frontend Neovim dla OpenCode - terminalowy agent kodujący AI |
+| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Dostawca Vercel AI SDK do używania OpenCode przez @opencode-ai/sdk |
+| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Aplikacja Web / Desktop i rozszerzenie VS Code dla OpenCode |
+| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Wtyczka Obsidian osadzająca OpenCode w interfejsie Obsidian |
+| [OpenWork](https://github.com/different-ai/openwork) | Otwartoźródłowa alternatywa dla Claude Cowork, napędzana przez OpenCode |
+| [ocx](https://github.com/kdcokenny/ocx) | Menedżer rozszerzeń OpenCode z przenośnymi, izolowanymi profilami. |
+| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Aplikacja kliencka Desktop, Web, Mobile i Remote dla OpenCode |
---
-## Agencja
+## Agenci
-| Imię | Opis |
-| ----------------------------------------------------------------- | ------------------------------------------------------------------------- |
-| [Agentic](https://github.com/Cluster444/agentic) | Modułowi agencje i polecenia AI do rozwoju strukturalnego |
-| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | Konfiguracje, podpowiedzi, agencje i wtyczki usprawniające przepływ pracy |
+| Nazwa | Opis |
+| ----------------------------------------------------------------- | ------------------------------------------------------------------------ |
+| [Agentic](https://github.com/Cluster444/agentic) | Modułowi agenci AI i komendy do strukturalnego rozwoju |
+| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | Konfiguracje, prompty, agenci i wtyczki dla ulepszonych przepływów pracy |
diff --git a/packages/web/src/content/docs/pt-br/ecosystem.mdx b/packages/web/src/content/docs/pt-br/ecosystem.mdx
index ac5d35444..64c38c4f0 100644
--- a/packages/web/src/content/docs/pt-br/ecosystem.mdx
+++ b/packages/web/src/content/docs/pt-br/ecosystem.mdx
@@ -1,12 +1,12 @@
---
title: Ecossistema
-description: Projetos e integrações construídos com o opencode.
+description: Projetos e integrações construídos com o OpenCode.
---
-Uma coleção de projetos da comunidade construídos sobre o opencode.
+Uma coleção de projetos da comunidade construídos sobre o OpenCode.
:::note
-Quer adicionar seu projeto relacionado ao opencode a esta lista? Envie um PR.
+Quer adicionar seu projeto relacionado ao OpenCode a esta lista? Envie um PR.
:::
Você também pode conferir [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) e [opencode.cafe](https://opencode.cafe), uma comunidade que agrega o ecossistema e a comunidade.
@@ -15,38 +15,39 @@ Você também pode conferir [awesome-opencode](https://github.com/awesome-openco
## Plugins
-| Nome | Descrição |
-| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Execute automaticamente sessões do opencode em sandboxes isoladas do Daytona com sincronização git e pré-visualizações ao vivo |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Injete automaticamente cabeçalhos de sessão Helicone para agrupamento de requisições |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-injetar tipos TypeScript/Svelte em leituras de arquivos com ferramentas de busca |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use sua assinatura ChatGPT Plus/Pro em vez de créditos de API |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use seu plano Gemini existente em vez de cobrança de API |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use os modelos gratuitos do Antigravity em vez de cobrança de API |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Isolamento de devcontainer multi-branch com clones rasos e portas atribuídas automaticamente |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Plugin Google Antigravity OAuth, com suporte para Google Search e manuseio de API mais robusto |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Otimize o uso de tokens podando saídas de ferramentas obsoletas |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Adicione suporte nativo de pesquisa na web para provedores suportados com estilo fundamentado no Google |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Permite que agentes de IA executem processos em segundo plano em um PTY, enviando entrada interativa para eles. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instruções para comandos de shell não interativos - evita travamentos de operações dependentes de TTY |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Acompanhe o uso do opencode com Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Limpe tabelas markdown produzidas por LLMs |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Edição de código 10x mais rápida com a API Morph Fast Apply e marcadores de edição preguiçosos |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agentes em segundo plano, ferramentas LSP/AST/MCP pré-construídas, agentes curados, compatível com Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notificações de desktop e alertas sonoros para sessões do opencode |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notificações de desktop e alertas sonoros para eventos de permissão, conclusão e erro |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Nomeação automática de sessões Zellij com suporte de IA com base no contexto do opencode |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Permite que agentes do opencode carreguem prompts sob demanda com descoberta e injeção de habilidades |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Memória persistente entre sessões usando Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Revisão de plano interativa com anotação visual e compartilhamento privado/offline |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Estenda opencode /commands em um poderoso sistema de orquestração com controle de fluxo granular |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Agende trabalhos recorrentes usando launchd (Mac) ou systemd (Linux) com sintaxe cron |
-| [micode](https://github.com/vtemian/micode) | Fluxo de trabalho Estruturado Brainstorm → Planejar → Implementar com continuidade de sessão |
-| [octto](https://github.com/vtemian/octto) | UI interativa do navegador para brainstorming de IA com formulários de múltiplas perguntas |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agentes em segundo plano estilo Claude Code com delegação assíncrona e persistência de contexto |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notificações nativas do OS para opencode – saiba quando as tarefas são concluídas |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Conjunto de orquestração multi-agente – 16 componentes, uma instalação |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Worktrees git sem atrito para opencode |
+| Nome | Descrição |
+| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Execute automaticamente sessões do OpenCode em sandboxes isoladas do Daytona com sincronização git e pré-visualizações ao vivo |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Injete automaticamente cabeçalhos de sessão Helicone para agrupamento de requisições |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-injetar tipos TypeScript/Svelte em leituras de arquivos com ferramentas de busca |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use sua assinatura ChatGPT Plus/Pro em vez de créditos de API |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use seu plano Gemini existente em vez de cobrança de API |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use os modelos gratuitos do Antigravity em vez de cobrança de API |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Isolamento de devcontainer multi-branch com clones rasos e portas atribuídas automaticamente |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Plugin Google Antigravity OAuth, com suporte para Google Search e manuseio de API mais robusto |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Otimize o uso de tokens podando saídas de ferramentas obsoletas |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Oculte segredos/PII em marcadores estilo VibeGuard antes de chamadas LLM; restaure localmente |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Adicione suporte nativo de pesquisa na web para provedores suportados com estilo fundamentado no Google |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Permite que agentes de IA executem processos em segundo plano em um PTY, enviando entrada interativa para eles. |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instruções para comandos de shell não interativos - evita travamentos de operações dependentes de TTY |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Acompanhe o uso do OpenCode com Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Limpe tabelas markdown produzidas por LLMs |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Edição de código 10x mais rápida com a API Morph Fast Apply e marcadores de edição preguiçosos |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agentes em segundo plano, ferramentas LSP/AST/MCP pré-construídas, agentes curados, compatível com Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notificações de desktop e alertas sonoros para sessões do OpenCode |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notificações de desktop e alertas sonoros para eventos de permissão, conclusão e erro |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Nomeação automática de sessões Zellij com suporte de IA com base no contexto do OpenCode |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Permite que agentes do OpenCode carreguem prompts sob demanda com descoberta e injeção de habilidades |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Memória persistente entre sessões usando Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Revisão de plano interativa com anotação visual e compartilhamento privado/offline |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Estenda opencode /commands em um poderoso sistema de orquestração com controle de fluxo granular |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Agende trabalhos recorrentes usando launchd (Mac) ou systemd (Linux) com sintaxe cron |
+| [micode](https://github.com/vtemian/micode) | Fluxo de trabalho Estruturado Brainstorm → Planejar → Implementar com continuidade de sessão |
+| [octto](https://github.com/vtemian/octto) | UI interativa do navegador para brainstorming de IA com formulários de múltiplas perguntas |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Agentes em segundo plano estilo Claude Code com delegação assíncrona e persistência de contexto |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notificações nativas do OS para OpenCode – saiba quando as tarefas são concluídas |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Conjunto de orquestração multi-agente – 16 componentes, uma instalação |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Worktrees git sem atrito para OpenCode |
---
@@ -54,17 +55,17 @@ Você também pode conferir [awesome-opencode](https://github.com/awesome-openco
| Nome | Descrição |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
-| [kimaki](https://github.com/remorses/kimaki) | Bot do Discord para controlar sessões do opencode, construído sobre o SDK |
+| [kimaki](https://github.com/remorses/kimaki) | Bot do Discord para controlar sessões do OpenCode, construído sobre o SDK |
| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Plugin Neovim para prompts cientes do editor, construído sobre a API |
-| [portal](https://github.com/hosenur/portal) | UI web mobile-first para opencode sobre Tailscale/VPN |
-| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Template para construir plugins do opencode |
-| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Frontend Neovim para opencode - um agente de codificação IA baseado em terminal |
-| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Provedor Vercel AI SDK para usar opencode via @opencode-ai/sdk |
-| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Aplicativo Web / Desktop e Extensão do VS Code para opencode |
-| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Plugin Obsidian que incorpora opencode na UI do Obsidian |
-| [OpenWork](https://github.com/different-ai/openwork) | Uma alternativa de código aberto ao Claude Cowork, alimentada pelo opencode |
-| [ocx](https://github.com/kdcokenny/ocx) | Gerenciador de extensões opencode com perfis portáteis e isolados. |
-| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Aplicativo Desktop, Web, Mobile e Cliente Remoto para opencode |
+| [portal](https://github.com/hosenur/portal) | UI web mobile-first para OpenCode sobre Tailscale/VPN |
+| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Template para construir plugins do OpenCode |
+| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Frontend Neovim para OpenCode - um agente de codificação IA baseado em terminal |
+| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Provedor Vercel AI SDK para usar OpenCode via @opencode-ai/sdk |
+| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Aplicativo Web / Desktop e Extensão do VS Code para OpenCode |
+| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Plugin Obsidian que incorpora OpenCode na UI do Obsidian |
+| [OpenWork](https://github.com/different-ai/openwork) | Uma alternativa de código aberto ao Claude Cowork, alimentada pelo OpenCode |
+| [ocx](https://github.com/kdcokenny/ocx) | Gerenciador de extensões OpenCode com perfis portáteis e isolados. |
+| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Aplicativo Desktop, Web, Mobile e Cliente Remoto para OpenCode |
---
diff --git a/packages/web/src/content/docs/ru/ecosystem.mdx b/packages/web/src/content/docs/ru/ecosystem.mdx
index a27804361..8b2d36685 100644
--- a/packages/web/src/content/docs/ru/ecosystem.mdx
+++ b/packages/web/src/content/docs/ru/ecosystem.mdx
@@ -1,12 +1,12 @@
---
title: Экосистема
-description: Проекты и интеграции, созданные с помощью opencode.
+description: Проекты и интеграции, созданные с помощью OpenCode.
---
-Коллекция проектов сообщества, построенных на opencode.
+Коллекция проектов сообщества, построенных на OpenCode.
:::note
-Хотите добавить свой проект, связанный с opencode, в этот список? Разместите PR.
+Хотите добавить свой проект, связанный с OpenCode, в этот список? Разместите PR.
:::
Вы также можете посетить [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) и [opencode.cafe](https://opencode.cafe) — хаб, объединяющий экосистему и сообщество.
@@ -15,61 +15,63 @@ description: Проекты и интеграции, созданные с по�
## Плагины
-| Имя | Описание |
-| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Автоматически запускайте сеансы opencode в изолированных песочницах Daytona с синхронизацией git и предварительным просмотром в реальном времени. |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Автоматически внедрять заголовки сеансов Helicone для группировки запросов. |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Автоматическое внедрение типов TypeScript/Svelte в файлы, считываемые с помощью инструментов поиска. |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Используйте подписку ChatGPT Plus/Pro вместо кредитов API. |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Используйте существующий план Gemini вместо выставления счетов через API. |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Используйте бесплатные модели Antigravity вместо выставления счетов через API. |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Многоветвевая изоляция контейнеров разработки с мелкими клонами и автоматическим назначением портов. |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Плагин Google Antigravity OAuth с поддержкой поиска Google и более надежной обработкой API. |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Оптимизируйте использование токенов за счет сокращения выходных данных устаревших инструментов. |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Добавьте встроенную поддержку веб-поиска для поддерживаемых поставщиков в стиле Google. |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Позволяет агентам ИИ запускать фоновые процессы в PTY и отправлять им интерактивные данные. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Инструкции для неинтерактивных shell-команд — предотвращают зависания из-за операций, зависящих от TTY. |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Отслеживайте использование opencode с помощью Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Очистка таблиц Markdown, созданных LLM |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Редактирование кода в 10 раз быстрее с помощью API Morph Fast Apply и маркеров отложенного редактирования. |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Фоновые агенты, встроенные инструменты LSP/AST/MCP, курируемые агенты, совместимость с Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Уведомления на рабочем столе и звуковые оповещения для сеансов opencode |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Уведомления на рабочем столе и звуковые оповещения о разрешениях, завершении и событиях ошибок. |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Автоматическое именование сеансов Zellij на основе искусственного интеллекта на основе контекста opencode. |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Разрешить агентам opencode отложенную загрузку подсказок по требованию с обнаружением и внедрением навыков. |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Постоянная память между сеансами с использованием Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Интерактивный обзор плана с визуальными аннотациями и возможностью совместного использования в частном или автономном режиме. |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Расширьте opencode/команды до мощной системы оркестровки с детальным управлением потоком данных. |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Планируйте повторяющиеся задания с помощью launchd (Mac) или systemd (Linux) с синтаксисом cron. |
-| [micode](https://github.com/vtemian/micode) | Структурированный мозговой штурм → План → Реализация рабочего процесса с непрерывностью сеанса |
-| [octto](https://github.com/vtemian/octto) | Интерактивный пользовательский интерфейс браузера для мозгового штурма с помощью искусственного интеллекта с формами из нескольких вопросов |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Фоновые агенты в стиле Claude Code с асинхронным делегированием и сохранением контекста. |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Встроенные уведомления ОС для opencode — узнайте, когда задачи завершены |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Комплексный пакет многоагентной оркестровки — 16 компонентов, одна установка |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Рабочие деревья git с нулевым трением для opencode |
+| Имя | Описание |
+| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Автоматически запускайте сеансы OpenCode в изолированных песочницах Daytona с синхронизацией git и предварительным просмотром в реальном времени |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Автоматически внедрять заголовки сеансов Helicone для группировки запросов |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Автоматическое внедрение типов TypeScript/Svelte в файлы, считываемые с помощью инструментов поиска |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Используйте подписку ChatGPT Plus/Pro вместо кредитов API |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Используйте существующий план Gemini вместо выставления счетов через API |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Используйте бесплатные модели Antigravity вместо выставления счетов через API |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Многоветвевая изоляция контейнеров разработки с мелкими клонами и автоматическим назначением портов |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Плагин Google Antigravity OAuth с поддержкой поиска Google и более надежной обработкой API |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Оптимизируйте использование токенов за счет сокращения выходных данных устаревших инструментов |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | Скрывайте секреты/PII, заменяя их плейсхолдерами в стиле VibeGuard перед отправкой в LLM; восстанавливайте локально |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Добавьте встроенную поддержку веб-поиска для поддерживаемых поставщиков в стиле Google |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Позволяет агентам ИИ запускать фоновые процессы в PTY и отправлять им интерактивные данные |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Инструкции для неинтерактивных shell-команд — предотвращают зависания из-за операций, зависящих от TTY |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Отслеживайте использование OpenCode с помощью Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Очистка таблиц Markdown, созданных LLM |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Редактирование кода в 10 раз быстрее с помощью API Morph Fast Apply и маркеров отложенного редактирования |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Фоновые агенты, встроенные инструменты LSP/AST/MCP, курируемые агенты, совместимость с Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Уведомления на рабочем столе и звуковые оповещения для сеансов OpenCode |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Уведомления на рабочем столе и звуковые оповещения о разрешениях, завершении и событиях ошибок |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | Автоматическое именование сеансов Zellij на основе искусственного интеллекта на основе контекста OpenCode |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Разрешить агентам OpenCode отложенную загрузку подсказок по требованию с обнаружением и внедрением навыков |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Постоянная память между сеансами с использованием Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Интерактивный обзор плана с визуальными аннотациями и возможностью совместного использования в частном или автономном режиме |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Расширьте opencode/команды до мощной системы оркестровки с детальным управлением потоком данных |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Планируйте повторяющиеся задания с помощью launchd (Mac) или systemd (Linux) с синтаксисом cron |
+| [micode](https://github.com/vtemian/micode) | Структурированный мозговой штурм → План → Реализация рабочего процесса с непрерывностью сеанса |
+| [octto](https://github.com/vtemian/octto) | Интерактивный пользовательский интерфейс браузера для мозгового штурма с помощью искусственного интеллекта с формами из нескольких вопросов |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Фоновые агенты в стиле Claude Code с асинхронным делегированием и сохранением контекста |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Встроенные уведомления ОС для OpenCode – узнайте, когда задачи завершены |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Комплексный пакет многоагентной оркестровки — 16 компонентов, одна установка |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Рабочие деревья git с нулевым трением для OpenCode |
---
## Проекты
-| Имя | Описание |
-| ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
-| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Плагин Neovim для подсказок с поддержкой редактора, созданный на основе API |
-| [portal](https://github.com/hosenur/portal) | Мобильный веб-интерфейс для opencode через Tailscale/VPN |
-| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Шаблон для создания плагинов opencode |
-| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Интерфейс Neovim для opencode — агент кодирования искусственного интеллекта на базе terminal |
-| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Поставщик Vercel AI SDK для использования opencode через @opencode-ai/sdk |
-| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Веб-приложение или настольное приложение и расширение VS Code для opencode |
-| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Плагин Obsidian, встраивающий opencode в пользовательский интерфейс Obsidian. |
-| [OpenWork](https://github.com/different-ai/openwork) | Альтернатива Claude Cowork с открытым исходным кодом на базе opencode. |
-| [ocx](https://github.com/kdcokenny/ocx) | Менеджер расширений opencode с переносимыми изолированными профилями. |
-| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Настольное, веб-, мобильное и удаленное клиентское приложение для opencode |
+| Имя | Описание |
+| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
+| [kimaki](https://github.com/remorses/kimaki) | Discord-бот для управления сеансами OpenCode, созданный на базе SDK |
+| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Плагин Neovim для подсказок с поддержкой редактора, созданный на основе API |
+| [portal](https://github.com/hosenur/portal) | Мобильный веб-интерфейс для OpenCode через Tailscale/VPN |
+| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Шаблон для создания плагинов OpenCode |
+| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Интерфейс Neovim для OpenCode - агент кодирования искусственного интеллекта на базе терминала |
+| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Поставщик Vercel AI SDK для использования OpenCode через @opencode-ai/sdk |
+| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Веб-приложение или настольное приложение и расширение VS Code для OpenCode |
+| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Плагин Obsidian, встраивающий OpenCode в пользовательский интерфейс Obsidian |
+| [OpenWork](https://github.com/different-ai/openwork) | Альтернатива Claude Cowork с открытым исходным кодом на базе OpenCode |
+| [ocx](https://github.com/kdcokenny/ocx) | Менеджер расширений OpenCode с переносимыми изолированными профилями |
+| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Настольное, веб-, мобильное и удаленное клиентское приложение для OpenCode |
---
## Агенты
-| Имя | Описание |
-| ----------------------------------------------------------------- | -------------------------------------------------------------------------- |
-| [Agentic](https://github.com/Cluster444/agentic) | Модульные ИИ-агенты и команды для структурированной разработки |
-| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | Конфигурации, подсказки, агенты и плагины для улучшения рабочих процессов. |
+| Имя | Описание |
+| ----------------------------------------------------------------- | ------------------------------------------------------------------------- |
+| [Agentic](https://github.com/Cluster444/agentic) | Модульные ИИ-агенты и команды для структурированной разработки |
+| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | Конфигурации, подсказки, агенты и плагины для улучшения рабочих процессов |
diff --git a/packages/web/src/content/docs/th/ecosystem.mdx b/packages/web/src/content/docs/th/ecosystem.mdx
index f1630f9a2..a46853486 100644
--- a/packages/web/src/content/docs/th/ecosystem.mdx
+++ b/packages/web/src/content/docs/th/ecosystem.mdx
@@ -15,38 +15,39 @@ description: โปรเจ็กต์และการผสานรวม�
## ปลั๊กอิน
-| ชื่อ | คำอธิบาย |
-| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | เรียกใช้เซสชัน OpenCode โดยอัตโนมัติในแซนด์บ็อกซ์ Daytona ที่แยกออกมาพร้อม git sync และการแสดงตัวอย่างแบบสด |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | แทรกส่วนหัวเซสชัน Helicone โดยอัตโนมัติสำหรับการจัดกลุ่มคำขอ |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | ฉีดประเภท TypeScript/Svelte ลงในไฟล์ที่อ่านโดยอัตโนมัติด้วยเครื่องมือค้นหา |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | ใช้การสมัครสมาชิก ChatGPT Plus/Pro แทนเครดิต API |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | ใช้แผน Gemini ที่มีอยู่ของคุณแทนการเรียกเก็บเงิน API |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | ใช้โมเดลฟรีของ Antigravity แทนการเรียกเก็บเงิน API |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | การแยกคอนเทนเนอร์ Devcontainer แบบหลายสาขาพร้อมโคลนแบบตื้นและพอร์ตที่กำหนดอัตโนมัติ |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | ปลั๊กอิน Google Antigravity OAuth พร้อมรองรับ Google Search และการจัดการ API ที่แข็งแกร่งยิ่งขึ้น |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | ปรับการใช้โทเค็นให้เหมาะสมโดยการตัดเอาท์พุตของเครื่องมือที่ล้าสมัย |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | เพิ่มการสนับสนุนการค้นหาเว็บแบบเนทีฟสำหรับผู้ให้บริการที่รองรับด้วยรูปแบบที่มีเหตุผลของ Google |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | ช่วยให้ตัวแทน AI สามารถเรียกใช้กระบวนการเบื้องหลังใน PTY และส่งข้อมูลเชิงโต้ตอบให้พวกเขาได้ |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | คำแนะนำสำหรับคำสั่ง shell แบบไม่โต้ตอบ - ป้องกันการแฮงค์จากการดำเนินการที่ขึ้นอยู่กับ TTY |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | ติดตามการใช้งาน OpenCode ด้วย Wakatime |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | ทำความสะอาดตาราง Markdown ที่ผลิตโดย LLM |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | การแก้ไขโค้ดเร็วขึ้น 10 เท่าด้วย Morph Fast Apply API และเครื่องหมายแก้ไขแบบ Lazy |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | ตัวแทนเบื้องหลัง, เครื่องมือ LSP/AST/MCP ที่สร้างไว้ล่วงหน้า, ตัวแทนที่ได้รับการดูแลจัดการ, เข้ากันได้กับ Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | การแจ้งเตือนบนเดสก์ท็อปและเสียงเตือนสำหรับเซสชัน OpenCode |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | การแจ้งเตือนบนเดสก์ท็อปและเสียงเตือนสำหรับการอนุญาต การดำเนินการเสร็จสิ้น และเหตุการณ์ข้อผิดพลาด |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | การตั้งชื่อเซสชัน Zellij อัตโนมัติที่ขับเคลื่อนด้วย AI ตามบริบทของ OpenCode |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | อนุญาตให้ตัวแทน OpenCode โหลดแบบ Lazy Load ตามความต้องการพร้อมการค้นพบทักษะและการแทรก |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | หน่วยความจำถาวรตลอดเซสชันโดยใช้ Supermemory |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | การตรวจสอบแผนเชิงโต้ตอบพร้อมคำอธิบายประกอบแบบภาพและการแชร์ส่วนตัว/offline |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | ขยาย opencode /commands ไปสู่ระบบการประสานที่มีประสิทธิภาพพร้อมการควบคุมโฟลว์แบบละเอียด |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | กำหนดเวลางานที่เกิดซ้ำโดยใช้ launchd (Mac) หรือ systemd (Linux) ด้วยไวยากรณ์ cron |
-| [micode](https://github.com/vtemian/micode) | ระดมความคิดอย่างมีโครงสร้าง → วางแผน → นำเวิร์กโฟลว์ไปใช้ด้วยความต่อเนื่องของเซสชัน |
-| [octto](https://github.com/vtemian/octto) | UI เบราว์เซอร์แบบโต้ตอบสำหรับการระดมความคิด AI ด้วยแบบฟอร์มคำถามหลายข้อ |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | เอเจนต์พื้นหลังสไตล์ Claude Code พร้อมการมอบหมายแบบอะซิงก์และการคงอยู่ของบริบท |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | การแจ้งเตือนระบบปฏิบัติการดั้งเดิมสำหรับ OpenCode – ทราบเมื่องานเสร็จสมบูรณ์ |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | ชุดสายรัดประสานหลายเอเจนต์ที่ให้มา – ส่วนประกอบ 16 ชิ้น ติดตั้งเพียงครั้งเดียว |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | เวิร์กทรีคอมไพล์ไร้แรงเสียดทานสำหรับ OpenCode |
+| ชื่อ | คำอธิบาย |
+| -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | เรียกใช้เซสชัน OpenCode โดยอัตโนมัติในแซนด์บ็อกซ์ Daytona ที่แยกออกมาพร้อม git sync และการแสดงตัวอย่างแบบสด |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | แทรกส่วนหัวเซสชัน Helicone โดยอัตโนมัติสำหรับการจัดกลุ่มคำขอ |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | ฉีดประเภท TypeScript/Svelte ลงในไฟล์ที่อ่านโดยอัตโนมัติด้วยเครื่องมือค้นหา |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | ใช้การสมัครสมาชิก ChatGPT Plus/Pro แทนเครดิต API |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | ใช้แผน Gemini ที่มีอยู่ของคุณแทนการเรียกเก็บเงิน API |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | ใช้โมเดลฟรีของ Antigravity แทนการเรียกเก็บเงิน API |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | การแยกคอนเทนเนอร์ Devcontainer แบบหลายสาขาพร้อมโคลนแบบตื้นและพอร์ตที่กำหนดอัตโนมัติ |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | ปลั๊กอิน Google Antigravity OAuth พร้อมรองรับ Google Search และการจัดการ API ที่แข็งแกร่งยิ่งขึ้น |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | ปรับการใช้โทเค็นให้เหมาะสมโดยการตัดเอาท์พุตของเครื่องมือที่ล้าสมัย |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | ปกปิดความลับ/PII เป็นตัวยึดตำแหน่งแบบ VibeGuard ก่อนการเรียก LLM และกู้คืนในเครื่อง |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | เพิ่มการสนับสนุนการค้นหาเว็บแบบเนทีฟสำหรับผู้ให้บริการที่รองรับด้วยรูปแบบที่มีเหตุผลของ Google |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | ช่วยให้ตัวแทน AI สามารถเรียกใช้กระบวนการเบื้องหลังใน PTY และส่งข้อมูลเชิงโต้ตอบให้พวกเขาได้ |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | คำแนะนำสำหรับคำสั่ง shell แบบไม่โต้ตอบ - ป้องกันการแฮงค์จากการดำเนินการที่ขึ้นอยู่กับ TTY |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | ติดตามการใช้งาน OpenCode ด้วย Wakatime |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | ทำความสะอาดตาราง Markdown ที่ผลิตโดย LLM |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | การแก้ไขโค้ดเร็วขึ้น 10 เท่าด้วย Morph Fast Apply API และเครื่องหมายแก้ไขแบบ Lazy |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | ตัวแทนเบื้องหลัง, เครื่องมือ LSP/AST/MCP ที่สร้างไว้ล่วงหน้า, ตัวแทนที่ได้รับการดูแลจัดการ, เข้ากันได้กับ Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | การแจ้งเตือนบนเดสก์ท็อปและเสียงเตือนสำหรับเซสชัน OpenCode |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | การแจ้งเตือนบนเดสก์ท็อปและเสียงเตือนสำหรับการอนุญาต การดำเนินการเสร็จสิ้น และเหตุการณ์ข้อผิดพลาด |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | การตั้งชื่อเซสชัน Zellij อัตโนมัติที่ขับเคลื่อนด้วย AI ตามบริบทของ OpenCode |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | อนุญาตให้ตัวแทน OpenCode โหลดแบบ Lazy Load ตามความต้องการพร้อมการค้นพบทักษะและการแทรก |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | หน่วยความจำถาวรตลอดเซสชันโดยใช้ Supermemory |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | การตรวจสอบแผนเชิงโต้ตอบพร้อมคำอธิบายประกอบแบบภาพและการแชร์ส่วนตัว/offline |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | ขยาย opencode /commands ไปสู่ระบบการประสานที่มีประสิทธิภาพพร้อมการควบคุมโฟลว์แบบละเอียด |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | กำหนดเวลางานที่เกิดซ้ำโดยใช้ launchd (Mac) หรือ systemd (Linux) ด้วยไวยากรณ์ cron |
+| [micode](https://github.com/vtemian/micode) | ระดมความคิดอย่างมีโครงสร้าง → วางแผน → นำเวิร์กโฟลว์ไปใช้ด้วยความต่อเนื่องของเซสชัน |
+| [octto](https://github.com/vtemian/octto) | UI เบราว์เซอร์แบบโต้ตอบสำหรับการระดมความคิด AI ด้วยแบบฟอร์มคำถามหลายข้อ |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | เอเจนต์พื้นหลังสไตล์ Claude Code พร้อมการมอบหมายแบบอะซิงก์และการคงอยู่ของบริบท |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | การแจ้งเตือนระบบปฏิบัติการดั้งเดิมสำหรับ OpenCode – ทราบเมื่องานเสร็จสมบูรณ์ |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | ชุดสายรัดประสานหลายเอเจนต์ที่ให้มา – ส่วนประกอบ 16 ชิ้น ติดตั้งเพียงครั้งเดียว |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | เวิร์กทรีคอมไพล์ไร้แรงเสียดทานสำหรับ OpenCode |
---
diff --git a/packages/web/src/content/docs/tr/ecosystem.mdx b/packages/web/src/content/docs/tr/ecosystem.mdx
index 835d9ba89..a3d34bd80 100644
--- a/packages/web/src/content/docs/tr/ecosystem.mdx
+++ b/packages/web/src/content/docs/tr/ecosystem.mdx
@@ -1,74 +1,75 @@
---
title: Ekosistem
-description: opencode ile ilgili tasarımlar ve entegrasyonlar.
+description: OpenCode ile geliştirilen projeler ve entegrasyonlar.
---
-opencode üzerine inşa edilmiş bir topluluk projeleri koleksiyonu.
+OpenCode üzerine inşa edilmiş topluluk projeleri koleksiyonu.
:::note
-opencode ile ilgili projenizi bu listeye eklemek ister misiniz? Bir PR gönderin.
+OpenCode ile ilgili projenizi bu listeye eklemek ister misiniz? Bir PR gönderin.
:::
-Ayrıca ekosistemi ve topluluğu bir araya getiren bir topluluk olan [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) ve [opencode.cafe](https://opencode.cafe)'e de göz atabilirsiniz.
+Ayrıca ekosistemi ve topluluğu bir araya getiren [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) ve [opencode.cafe](https://opencode.cafe) adreslerine de göz atabilirsiniz.
---
## Eklentiler
-| İsim | Açıklama |
-| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | opencode oturumlarını git senkronizasyonu ve canlı önizlemelerle izole Daytona sanal alanlarında otomatik olarak çalıştırın |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | İstek gruplaması için Helicone oturum başlıklarını otomatik olarak ekleme |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Arama araçlarıyla TypeScript/Svelte türlerini dosya okumalarına otomatik olarak enjekte edin |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | API kredisi yerine ChatGPT Plus/Pro aboneliğinizi kullanın |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | API faturalandırma yerine mevcut Gemini planınızı kullanın |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | API faturalandırma yerine Antigravity'nin ücretsiz modellerini kullanın |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Sığ klonlar ve otomatik atanan bağlantı noktalarıyla çok dallı devcontainer izolasyonu |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Arama desteği ve daha sağlam API işleme özelliğiyle Google Antigravity OAuth Eklentisi |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Desteklenen sağlayıcılar için Google tabanlı stil ile yerel web araması desteği ekleyin |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Yapay zeka aracılarının bir PTY'de arka plan işlemlerini çalıştırmasına ve onlara etkileşimli girdi göndermesine olanak tanır. |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Etkileşimli olmayan kabuk komutlarına yönelik talimatlar - TTY bağımlı işlemlerden kaynaklanan askıda kalmaları önler |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Wakatime ile opencode kullanımını izleyin |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast Apply API ve yavaş düzenleme işaretçileriyle 10 kat daha hızlı kod düzenleme |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | opencode oturumları için masaüstü bildirimleri ve sesli uyarılar |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | İzin, tamamlama ve hata olayları için masaüstü bildirimleri ve sesli uyarılar |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | opencode bağlamına dayalı yapay zeka destekli otomatik Zellij oturumu adlandırma |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | opencode temsilcilerinin, beceri keşfi ve ekleme ile istek üzerine istemleri yavaş yüklemesine izin verin |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Supermemory kullanarak oturumlar arasında kalıcı hafıza |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Görsel açıklama ve private/offline paylaşımıyla etkileşimli plan incelemesi |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | opencode'u/komutları ayrıntılı akış kontrolüyle güçlü bir orkestrasyon sistemine genişletin |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Cron sözdizimi ile launchd (Mac) veya systemd (Linux) kullanarak yinelenen işleri planlayın |
-| [micode](https://github.com/vtemian/micode) | Yapılandırılmış Beyin Fırtınası → Planla → Oturum sürekliliği ile iş akışını uygulama |
-| [octto](https://github.com/vtemian/octto) | Çoklu soru formlarıyla yapay zeka beyin fırtınası için etkileşimli tarayıcı arayüzü |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Eşzamansız delegasyon ve bağlam kalıcılığına sahip Claude Code tarzı arka plan aracıları |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | opencode için yerel işletim sistemi bildirimleri – görevlerin ne zaman tamamlandığını bilin |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Birlikte verilen çok aracılı orkestrasyon donanımı – 16 bileşen, tek kurulum |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | opencode için sıfır sürtünmeli git çalışma ağaçları |
+| İsim | Açıklama |
+| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | OpenCode oturumlarını, git senkronizasyonu ve canlı önizlemelerle izole Daytona sanal alanlarında otomatik olarak çalıştırın |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | İstek gruplaması için Helicone oturum başlıklarını otomatik olarak ekleyin |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Arama araçlarıyla TypeScript/Svelte türlerini dosya okumalarına otomatik olarak enjekte edin |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | API kredisi yerine ChatGPT Plus/Pro aboneliğinizi kullanın |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | API faturalandırması yerine mevcut Gemini planınızı kullanın |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | API faturalandırması yerine Antigravity'nin ücretsiz modellerini kullanın |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Sığ klonlar ve otomatik atanan portlarla çok dallı devcontainer izolasyonu |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Arama desteği ve daha sağlam API işleme özelliğiyle Google Antigravity OAuth Eklentisi |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Eski araç çıktılarını budayarak token kullanımını optimize edin |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | LLM çağrılarından önce sırları/kişisel verileri VibeGuard tarzı yer tutucularla gizleyin; yerel olarak geri yükleyin |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Desteklenen sağlayıcılar için Google kaynaklı stil ile yerel web araması desteği ekleyin |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Yapay zeka aracılarının bir PTY'de arka plan işlemlerini çalıştırmasına ve onlara etkileşimli girdi göndermesine olanak tanır |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Etkileşimli olmayan kabuk komutları için talimatlar - TTY bağımlı işlemlerden kaynaklanan takılmaları önler |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Wakatime ile OpenCode kullanımını takip edin |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLM'ler tarafından üretilen markdown tablolarını temizleyin |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast Apply API ve tembel düzenleme işaretçileriyle 10 kat daha hızlı kod düzenleme |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Arka plan aracıları, hazır LSP/AST/MCP araçları, seçilmiş aracılar, Claude Code uyumlu |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode oturumları için masaüstü bildirimleri ve sesli uyarılar |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | İzin, tamamlanma ve hata olayları için masaüstü bildirimleri ve sesli uyarılar |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | OpenCode bağlamına dayalı yapay zeka destekli otomatik Zellij oturum isimlendirmesi |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | OpenCode aracılarının, beceri keşfi ve enjeksiyonu ile istemleri talep üzerine tembel yüklemesine (lazy load) izin verin |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Supermemory kullanarak oturumlar arası kalıcı hafıza |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Görsel not alma ve özel/çevrimdışı paylaşım ile etkileşimli plan incelemesi |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | opencode /commands komutlarını, ayrıntılı akış kontrolü ile güçlü bir orkestrasyon sistemine genişletin |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Launchd (Mac) veya systemd (Linux) kullanarak cron sözdizimi ile tekrarlayan işler planlayın |
+| [micode](https://github.com/vtemian/micode) | Oturum sürekliliği ile Yapılandırılmış Beyin Fırtınası → Planlama → Uygulama iş akışı |
+| [octto](https://github.com/vtemian/octto) | Çok sorulu formlarla yapay zeka beyin fırtınası için etkileşimli tarayıcı arayüzü |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Asenkron delegasyon ve bağlam kalıcılığına sahip Claude Code tarzı arka plan aracıları |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode için yerel işletim sistemi bildirimleri – görevlerin ne zaman tamamlandığını bilin |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Paketlenmiş çoklu aracı orkestrasyon donanımı – 16 bileşen, tek kurulum |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode için sıfır sürtünmeli git çalışma ağaçları (worktrees) |
---
## Projeler
-| İsim | Tanım |
-| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
-| [kimaki](https://github.com/remorses/kimaki) | SDK üzerine kurulu opencode oturumlarını kontrol eden Discord botu |
-| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | API temel alınarak oluşturulmuş, editöre duyarlı istemler için Neovim eklentisi |
-| [portal](https://github.com/hosenur/portal) | Tailscale/VPN üzerinden opencode için mobil öncelikli web kullanıcı arayüzü |
-| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | opencode eklentileri oluşturmak için şablon |
-| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | opencode için Neovim ön ucu - terminal tabanlı bir AI kodlama aracısı |
-| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | @opencode-ai/sdk aracılığıyla opencode'u kullanmak için Vercel AI SDK sağlayıcısı |
-| [OpenChamber](https://github.com/btriapitsyn/openchamber) | opencode için Web / Masaüstü Uygulaması ve VS Code Uzantısı |
-| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | opencode'u Obsidian'ın kullanıcı arayüzüne yerleştiren Obsidian eklentisi |
-| [OpenWork](https://github.com/different-ai/openwork) | opencode tarafından desteklenen, Claude Cowork'e açık kaynaklı bir alternatif |
-| [ocx](https://github.com/kdcokenny/ocx) | Taşınabilir, yalıtılmış profillere sahip opencode uzantı yöneticisi. |
-| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | opencode için Masaüstü, Web, Mobil ve Uzak İstemci Uygulaması |
+| İsim | Açıklama |
+| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
+| [kimaki](https://github.com/remorses/kimaki) | SDK üzerine inşa edilmiş, OpenCode oturumlarını kontrol eden Discord botu |
+| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | API üzerine inşa edilmiş, editör farkındalıklı istemler için Neovim eklentisi |
+| [portal](https://github.com/hosenur/portal) | Tailscale/VPN üzerinden OpenCode için mobil öncelikli web arayüzü |
+| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | OpenCode eklentileri oluşturmak için şablon |
+| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | opencode için Neovim ön yüzü - terminal tabanlı bir yapay zeka kodlama aracısı |
+| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | @opencode-ai/sdk aracılığıyla OpenCode kullanmak için Vercel AI SDK sağlayıcısı |
+| [OpenChamber](https://github.com/btriapitsyn/openchamber) | OpenCode için Web / Masaüstü Uygulaması ve VS Code Uzantısı |
+| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | OpenCode'u Obsidian arayüzüne gömen Obsidian eklentisi |
+| [OpenWork](https://github.com/different-ai/openwork) | OpenCode tarafından desteklenen, Claude Cowork'e açık kaynaklı bir alternatif |
+| [ocx](https://github.com/kdcokenny/ocx) | Taşınabilir, izole profillere sahip OpenCode eklenti yöneticisi |
+| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | OpenCode için Masaüstü, Web, Mobil ve Uzak İstemci Uygulaması |
---
-## Agent'lar
+## Aracılar
| İsim | Açıklama |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------- |
diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx
index 6b1b21951..5ed2125cb 100644
--- a/packages/web/src/content/docs/zen.mdx
+++ b/packages/web/src/content/docs/zen.mdx
@@ -121,12 +121,12 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| --------------------------------- | ------ | ------ | ----------- | ------------ |
| Big Pickle | Free | Free | Free | - |
| MiniMax M2.5 Free | Free | Free | Free | - |
-| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - |
+| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - |
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
-| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - |
+| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
diff --git a/packages/web/src/content/docs/zh-cn/ecosystem.mdx b/packages/web/src/content/docs/zh-cn/ecosystem.mdx
index c77cc0542..24a5fbe87 100644
--- a/packages/web/src/content/docs/zh-cn/ecosystem.mdx
+++ b/packages/web/src/content/docs/zh-cn/ecosystem.mdx
@@ -15,38 +15,39 @@ description: 基于 OpenCode 构建的项目与集成。
## 插件
-| 名称 | 描述 |
-| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | 在隔离的 Daytona 沙箱中自动运行 OpenCode 会话,支持 git 同步和实时预览 |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | 自动注入 Helicone 会话头信息,用于请求分组 |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | 通过查找工具自动将 TypeScript/Svelte 类型注入到文件读取中 |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | 使用您的 ChatGPT Plus/Pro 订阅替代 API 额度 |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | 使用您现有的 Gemini 套餐替代 API 计费 |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | 使用 Antigravity 的免费模型替代 API 计费 |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | 多分支开发容器隔离,支持浅克隆和自动分配端口 |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth 插件,支持 Google 搜索及更强健的 API 处理 |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 通过修剪过时的工具输出来优化 Token 使用 |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | 为受支持的提供商添加原生网页搜索支持,采用 Google grounded 风格 |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | 使 AI 代理能够在 PTY 中运行后台进程,并向其发送交互式输入 |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 非交互式 shell 命令指令——防止依赖 TTY 的操作导致挂起 |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | 使用 Wakatime 追踪 OpenCode 的使用情况 |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | 清理 LLM 生成的 Markdown 表格 |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 通过 Morph Fast Apply API 和惰性编辑标记实现 10 倍更快的代码编辑 |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | 后台代理、预构建的 LSP/AST/MCP 工具、精选代理,兼容 Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 会话的桌面通知和声音提醒 |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 针对权限请求、任务完成和错误事件的桌面通知与声音提醒 |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | 基于 OpenCode 上下文的 AI 驱动自动 Zellij 会话命名 |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | 允许 OpenCode 代理通过技能发现和注入按需延迟加载提示词 |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | 使用 Supermemory 实现跨会话的持久记忆 |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | 支持可视化标注和私有/离线分享的交互式计划审查 |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | 将 OpenCode /commands 扩展为具有精细流程控制的强大编排系统 |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | 使用 cron 语法通过 launchd (Mac) 或 systemd (Linux) 调度周期性任务 |
-| [micode](https://github.com/vtemian/micode) | 结构化的头脑风暴 → 计划 → 实现工作流,支持会话连续性 |
-| [octto](https://github.com/vtemian/octto) | 用于 AI 头脑风暴的交互式浏览器 UI,支持多问题表单 |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code 风格的后台代理,支持异步委托和上下文持久化 |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode 的原生操作系统通知——随时了解任务完成情况 |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | 捆绑式多代理编排套件——16 个组件,一次安装 |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode 的零摩擦 git worktree 管理 |
+| 名称 | 描述 |
+| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | 在隔离的 Daytona 沙箱中自动运行 OpenCode 会话,支持 git 同步和实时预览 |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | 自动注入 Helicone 会话头信息,用于请求分组 |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | 通过查找工具自动将 TypeScript/Svelte 类型注入到文件读取中 |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | 使用您的 ChatGPT Plus/Pro 订阅替代 API 额度 |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | 使用您现有的 Gemini 套餐替代 API 计费 |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | 使用 Antigravity 的免费模型替代 API 计费 |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | 多分支开发容器隔离,支持浅克隆和自动分配端口 |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth 插件,支持 Google 搜索及更强健的 API 处理 |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 通过修剪过时的工具输出来优化 Token 使用 |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | 在调用 LLM 之前将机密/PII 替换为 VibeGuard 风格的占位符;并在本地恢复 |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | 为受支持的提供商添加原生网页搜索支持,采用 Google grounded 风格 |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | 使 AI 代理能够在 PTY 中运行后台进程,并向其发送交互式输入 |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 非交互式 shell 命令指令——防止依赖 TTY 的操作导致挂起 |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | 使用 Wakatime 追踪 OpenCode 的使用情况 |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | 清理 LLM 生成的 Markdown 表格 |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 通过 Morph Fast Apply API 和惰性编辑标记实现 10 倍更快的代码编辑 |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | 后台代理、预构建的 LSP/AST/MCP 工具、精选代理,兼容 Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 会话的桌面通知和声音提醒 |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 针对权限请求、任务完成和错误事件的桌面通知与声音提醒 |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | 基于 OpenCode 上下文的 AI 驱动自动 Zellij 会话命名 |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | 允许 OpenCode 代理通过技能发现和注入按需延迟加载提示词 |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | 使用 Supermemory 实现跨会话的持久记忆 |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | 支持可视化标注和私有/离线分享的交互式计划审查 |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | 将 OpenCode /commands 扩展为具有精细流程控制的强大编排系统 |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | 使用 cron 语法通过 launchd (Mac) 或 systemd (Linux) 调度周期性任务 |
+| [micode](https://github.com/vtemian/micode) | 结构化的头脑风暴 → 计划 → 实现工作流,支持会话连续性 |
+| [octto](https://github.com/vtemian/octto) | 用于 AI 头脑风暴的交互式浏览器 UI,支持多问题表单 |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code 风格的后台代理,支持异步委托和上下文持久化 |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode 的原生操作系统通知——随时了解任务完成情况 |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | 捆绑式多代理编排套件——16 个组件,一次安装 |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode 的零摩擦 git worktree 管理 |
---
diff --git a/packages/web/src/content/docs/zh-tw/ecosystem.mdx b/packages/web/src/content/docs/zh-tw/ecosystem.mdx
index 3a867d143..50871355e 100644
--- a/packages/web/src/content/docs/zh-tw/ecosystem.mdx
+++ b/packages/web/src/content/docs/zh-tw/ecosystem.mdx
@@ -15,38 +15,39 @@ description: 基於 OpenCode 建置的專案與整合。
## 外掛程式
-| 名稱 | 說明 |
-| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
-| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | 在隔離的 Daytona 沙箱中自動執行 OpenCode 工作階段,支援 git 同步和即時預覽 |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | 自動注入 Helicone 工作階段標頭資訊,用於請求分組 |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | 透過搜尋工具自動將 TypeScript/Svelte 型別注入到檔案讀取中 |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | 使用您的 ChatGPT Plus/Pro 訂閱替代 API 額度 |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | 使用您現有的 Gemini 方案替代 API 計費 |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | 使用 Antigravity 的免費模型替代 API 計費 |
-| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | 多分支開發容器隔離,支援淺層複製和自動分配連接埠 |
-| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth 外掛程式,支援 Google 搜尋及更強健的 API 處理 |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 透過修剪過時的工具輸出來最佳化 Token 使用 |
-| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | 為受支援的供應商新增原生網頁搜尋支援,採用 Google grounded 風格 |
-| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | 使 AI 代理能夠在 PTY 中執行背景處理程序,並向其傳送互動式輸入 |
-| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 非互動式 shell 指令說明——防止依賴 TTY 的操作導致卡住 |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | 使用 Wakatime 追蹤 OpenCode 的使用情況 |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | 清理 LLM 生成的 Markdown 表格 |
-| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 透過 Morph Fast Apply API 和惰性編輯標記實現 10 倍更快的程式碼編輯 |
-| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | 背景代理、預建置的 LSP/AST/MCP 工具、精選代理,相容 Claude Code |
-| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 工作階段的桌面通知和聲音提醒 |
-| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 針對權限請求、任務完成和錯誤事件的桌面通知與聲音提醒 |
-| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | 基於 OpenCode 上下文的 AI 驅動自動 Zellij 工作階段命名 |
-| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | 允許 OpenCode 代理透過技能發現和注入按需延遲載入提示詞 |
-| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | 使用 Supermemory 實現跨工作階段的持久記憶 |
-| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | 支援視覺化標註和私有/離線分享的互動式計畫審查 |
-| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | 將 OpenCode /commands 擴展為具有精細流程控制的強大編排系統 |
-| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | 使用 cron 語法透過 launchd (Mac) 或 systemd (Linux) 排程週期性任務 |
-| [micode](https://github.com/vtemian/micode) | 結構化的腦力激盪 → 計畫 → 實作工作流程,支援工作階段連續性 |
-| [octto](https://github.com/vtemian/octto) | 用於 AI 腦力激盪的互動式瀏覽器 UI,支援多問題表單 |
-| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code 風格的背景代理,支援非同步委派和上下文持久化 |
-| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode 的原生作業系統通知——隨時了解任務完成情況 |
-| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | 捆綁式多代理編排套件——16 個元件,一次安裝 |
-| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode 的零摩擦 git worktree 管理 |
+| 名稱 | 說明 |
+| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
+| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | 在隔離的 Daytona 沙箱中自動執行 OpenCode 工作階段,支援 git 同步和即時預覽 |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | 自動注入 Helicone 工作階段標頭資訊,用於請求分組 |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | 透過搜尋工具自動將 TypeScript/Svelte 型別注入到檔案讀取中 |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | 使用您的 ChatGPT Plus/Pro 訂閱替代 API 額度 |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | 使用您現有的 Gemini 方案替代 API 計費 |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | 使用 Antigravity 的免費模型替代 API 計費 |
+| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | 多分支開發容器隔離,支援淺層複製和自動分配連接埠 |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth 外掛程式,支援 Google 搜尋及更強健的 API 處理 |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 透過修剪過時的工具輸出來最佳化 Token 使用 |
+| [opencode-vibeguard](https://github.com/inkdust2021/opencode-vibeguard) | 在呼叫 LLM 之前將秘密/PII 編輯為 VibeGuard 風格的預留位置;並在本地還原 |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | 為受支援的供應商新增原生網頁搜尋支援,採用 Google grounded 風格 |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | 使 AI 代理能夠在 PTY 中執行背景處理程序,並向其傳送互動式輸入 |
+| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 非互動式 shell 指令說明——防止依賴 TTY 的操作導致卡住 |
+| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | 使用 Wakatime 追蹤 OpenCode 的使用情況 |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | 清理 LLM 生成的 Markdown 表格 |
+| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 透過 Morph Fast Apply API 和惰性編輯標記實現 10 倍更快的程式碼編輯 |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | 背景代理、預建置的 LSP/AST/MCP 工具、精選代理,相容 Claude Code |
+| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 工作階段的桌面通知和聲音提醒 |
+| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 針對權限請求、任務完成和錯誤事件的桌面通知與聲音提醒 |
+| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | 基於 OpenCode 上下文的 AI 驅動自動 Zellij 工作階段命名 |
+| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | 允許 OpenCode 代理透過技能發現和注入按需延遲載入提示詞 |
+| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | 使用 Supermemory 實現跨工作階段的持久記憶 |
+| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | 支援視覺化標註和私有/離線分享的互動式計畫審查 |
+| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | 將 OpenCode /commands 擴展為具有精細流程控制的強大編排系統 |
+| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | 使用 cron 語法透過 launchd (Mac) 或 systemd (Linux) 排程週期性任務 |
+| [micode](https://github.com/vtemian/micode) | 結構化的腦力激盪 → 計畫 → 實作工作流程,支援工作階段連續性 |
+| [octto](https://github.com/vtemian/octto) | 用於 AI 腦力激盪的互動式瀏覽器 UI,支援多問題表單 |
+| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code 風格的背景代理,支援非同步委派和上下文持久化 |
+| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode 的原生作業系統通知——隨時了解任務完成情況 |
+| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | 捆綁式多代理編排套件——16 個元件,一次安裝 |
+| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode 的零摩擦 git worktree 管理 |
---