-
+
-
+
+ )
+
createEffect(
on(
() => tabs().active(),
@@ -1141,6 +1223,7 @@ export default function Page() {
const focusReviewDiff = (path: string) => {
const current = view().review.open() ?? []
if (!current.includes(path)) view().review.setOpen([...current, path])
+ setActiveDiff(path)
setPendingDiff(path)
}
@@ -1188,33 +1271,70 @@ export default function Page() {
const activeTab = createMemo(() => {
const active = tabs().active()
if (active === "context") return "context"
+ if (active === "review" && reviewTab()) return "review"
if (active && file.pathFromTab(active)) return normalizeTab(active)
const first = openedTabs()[0]
if (first) return first
if (contextOpen()) return "context"
+ if (reviewTab() && hasReview()) return "review"
return "empty"
})
createEffect(() => {
if (!layout.ready()) return
if (tabs().active()) return
- if (openedTabs().length === 0 && !contextOpen()) return
+ if (openedTabs().length === 0 && !contextOpen() && !(reviewTab() && hasReview())) return
const next = activeTab()
if (next === "empty") return
tabs().setActive(next)
})
+ createEffect(
+ on(
+ () => layout.fileTree.opened(),
+ (opened, prev) => {
+ if (prev === undefined) return
+ if (!isDesktop()) return
+
+ if (opened) {
+ const active = tabs().active()
+ const tab = active === "review" || (!active && hasReview()) ? "changes" : "all"
+ layout.fileTree.setTab(tab)
+ return
+ }
+
+ if (fileTreeTab() !== "changes") return
+ tabs().setActive("review")
+ },
+ { defer: true },
+ ),
+ )
+
createEffect(() => {
const id = params.id
if (!id) return
- const wants = isDesktop() ? fileTreeTab() === "changes" : store.mobileTab === "changes"
+ const wants = isDesktop()
+ ? layout.fileTree.opened()
+ ? fileTreeTab() === "changes"
+ : activeTab() === "review"
+ : store.mobileTab === "changes"
if (!wants) return
if (sync.data.session_diff[id] !== undefined) return
+ if (sync.status === "loading") return
- sync.session.diff(id)
+ void sync.session.diff(id)
+ })
+
+ createEffect(() => {
+ if (!isDesktop()) return
+ if (!layout.fileTree.opened()) return
+ if (sync.status === "loading") return
+
+ fileTreeTab()
+ void file.tree.list("")
})
const autoScroll = createAutoScroll({
@@ -1222,9 +1342,15 @@ export default function Page() {
overflowAnchor: "dynamic",
})
+ const clearMessageHash = () => {
+ if (!window.location.hash) return
+ window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
+ }
+
const resumeScroll = () => {
setStore("messageId", undefined)
autoScroll.forceScrollToBottom()
+ clearMessageHash()
}
// When the user returns to the bottom, treat the active message as "latest".
@@ -1234,6 +1360,7 @@ export default function Page() {
(scrolled) => {
if (scrolled) return
setStore("messageId", undefined)
+ clearMessageHash()
},
{ defer: true },
),
@@ -1308,7 +1435,8 @@ export default function Page() {
requestAnimationFrame(() => {
const delta = el.scrollHeight - beforeHeight
- if (delta) el.scrollTop = beforeTop + delta
+ if (!delta) return
+ el.scrollTop = beforeTop + delta
})
scheduleTurnBackfill()
@@ -1439,6 +1567,7 @@ export default function Page() {
const match = hash.match(/^message-(.+)$/)
if (match) {
+ autoScroll.pause()
const msg = visibleUserMessages().find((m) => m.id === match[1])
if (msg) {
scrollToMessage(msg, behavior)
@@ -1452,6 +1581,7 @@ export default function Page() {
const target = document.getElementById(hash)
if (target) {
+ autoScroll.pause()
scrollToElement(target, behavior)
return
}
@@ -1548,6 +1678,7 @@ export default function Page() {
const msg = visibleUserMessages().find((m) => m.id === targetId)
if (!msg) return
if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined)
+ autoScroll.pause()
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
})
@@ -1687,6 +1818,7 @@ export default function Page() {
diffs={diffs}
view={view}
diffStyle="unified"
+ focusedFile={activeDiff()}
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
comments={comments.all()}
focusedComment={comments.focus()}
@@ -1727,27 +1859,102 @@ export default function Page() {
>
markScrollGesture(e.target)}
- onTouchMove={(e) => markScrollGesture(e.target)}
+ onWheel={(e) => {
+ const root = e.currentTarget
+ const target = e.target instanceof Element ? e.target : undefined
+ const nested = target?.closest("[data-scrollable]")
+ if (!nested || nested === root) {
+ markScrollGesture(root)
+ return
+ }
+
+ if (!(nested instanceof HTMLElement)) {
+ markScrollGesture(root)
+ return
+ }
+
+ const max = nested.scrollHeight - nested.clientHeight
+ if (max <= 1) {
+ markScrollGesture(root)
+ return
+ }
+
+ const delta =
+ e.deltaMode === 1
+ ? e.deltaY * 40
+ : e.deltaMode === 2
+ ? e.deltaY * root.clientHeight
+ : e.deltaY
+ if (!delta) return
+
+ if (delta < 0) {
+ if (nested.scrollTop + delta <= 0) markScrollGesture(root)
+ return
+ }
+
+ const remaining = max - nested.scrollTop
+ if (delta > remaining) markScrollGesture(root)
+ }}
+ onTouchStart={(e) => {
+ touchGesture = e.touches[0]?.clientY
+ }}
+ onTouchMove={(e) => {
+ const next = e.touches[0]?.clientY
+ const prev = touchGesture
+ touchGesture = next
+ if (next === undefined || prev === undefined) return
+
+ const delta = prev - next
+ if (!delta) return
+
+ const root = e.currentTarget
+ const target = e.target instanceof Element ? e.target : undefined
+ const nested = target?.closest("[data-scrollable]")
+ if (!nested || nested === root) {
+ markScrollGesture(root)
+ return
+ }
+
+ if (!(nested instanceof HTMLElement)) {
+ markScrollGesture(root)
+ return
+ }
+
+ const max = nested.scrollHeight - nested.clientHeight
+ if (max <= 1) {
+ markScrollGesture(root)
+ return
+ }
+
+ if (delta < 0) {
+ if (nested.scrollTop + delta <= 0) markScrollGesture(root)
+ return
+ }
+
+ const remaining = max - nested.scrollTop
+ if (delta > remaining) markScrollGesture(root)
+ }}
+ onTouchEnd={() => {
+ touchGesture = undefined
+ }}
+ onTouchCancel={() => {
+ touchGesture = undefined
+ }}
onPointerDown={(e) => {
if (e.target !== e.currentTarget) return
- markScrollGesture(e.target)
+ markScrollGesture(e.currentTarget)
}}
onScroll={(e) => {
- autoScroll.handleScroll()
if (!hasScrollGesture()) return
- markScrollGesture(e.target)
+ autoScroll.handleScroll()
+ markScrollGesture(e.currentTarget)
if (isDesktop()) scheduleScrollSpy(e.currentTarget)
}}
onClick={autoScroll.handleInteraction}
@@ -1976,7 +2183,7 @@ export default function Page() {
>
+
+
+
+
+
+
+
+
{language.t("session.tab.review")}
+
+
+ {reviewCount()}
+
+
+
+
+
+
+
+
+
+ {reviewPanel()}
+
+
+
@@ -2571,46 +2802,7 @@ export default function Page() {
}
>
-
-
-
-
- {language.t("session.review.loadingChanges")}
- }
- >
-
addCommentToContext({ ...comment, origin: "review" })}
- comments={comments.all()}
- focusedComment={comments.focus()}
- onFocusedCommentChange={comments.setFocus}
- onViewFile={(path) => {
- showAllFiles()
- const value = file.tab(path)
- tabs().open(value)
- file.load(path)
- }}
- />
-
-
-
-
-
-
- {language.t("session.review.empty")}
-
-
-
-
-
-
+ {reviewPanel()}
@@ -2652,8 +2844,9 @@ export default function Page() {
focusReviewDiff(node.path)}
/>
@@ -2669,7 +2862,7 @@ export default function Page() {
openTab(file.tab(node.path))}
/>
diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts
index 129695f8649..0ca3abad069 100644
--- a/packages/app/src/utils/persist.ts
+++ b/packages/app/src/utils/persist.ts
@@ -18,7 +18,52 @@ const LEGACY_STORAGE = "default.dat"
const GLOBAL_STORAGE = "opencode.global.dat"
const LOCAL_PREFIX = "opencode."
const fallback = { disabled: false }
-const cache = new Map()
+
+const CACHE_MAX_ENTRIES = 500
+const CACHE_MAX_BYTES = 8 * 1024 * 1024
+
+type CacheEntry = { value: string; bytes: number }
+const cache = new Map()
+const cacheTotal = { bytes: 0 }
+
+function cacheDelete(key: string) {
+ const entry = cache.get(key)
+ if (!entry) return
+ cacheTotal.bytes -= entry.bytes
+ cache.delete(key)
+}
+
+function cachePrune() {
+ for (;;) {
+ if (cache.size <= CACHE_MAX_ENTRIES && cacheTotal.bytes <= CACHE_MAX_BYTES) return
+ const oldest = cache.keys().next().value as string | undefined
+ if (!oldest) return
+ cacheDelete(oldest)
+ }
+}
+
+function cacheSet(key: string, value: string) {
+ const bytes = value.length * 2
+ if (bytes > CACHE_MAX_BYTES) {
+ cacheDelete(key)
+ return
+ }
+
+ const entry = cache.get(key)
+ if (entry) cacheTotal.bytes -= entry.bytes
+ cache.delete(key)
+ cache.set(key, { value, bytes })
+ cacheTotal.bytes += bytes
+ cachePrune()
+}
+
+function cacheGet(key: string) {
+ const entry = cache.get(key)
+ if (!entry) return
+ cache.delete(key)
+ cache.set(key, entry)
+ return entry.value
+}
function quota(error: unknown) {
if (error instanceof DOMException) {
@@ -63,9 +108,11 @@ function evict(storage: Storage, keep: string, value: string) {
for (const item of items) {
storage.removeItem(item.key)
+ cacheDelete(item.key)
try {
storage.setItem(keep, value)
+ cacheSet(keep, value)
return true
} catch (error) {
if (!quota(error)) throw error
@@ -78,6 +125,7 @@ function evict(storage: Storage, keep: string, value: string) {
function write(storage: Storage, key: string, value: string) {
try {
storage.setItem(key, value)
+ cacheSet(key, value)
return true
} catch (error) {
if (!quota(error)) throw error
@@ -85,13 +133,17 @@ function write(storage: Storage, key: string, value: string) {
try {
storage.removeItem(key)
+ cacheDelete(key)
storage.setItem(key, value)
+ cacheSet(key, value)
return true
} catch (error) {
if (!quota(error)) throw error
}
- return evict(storage, key, value)
+ const ok = evict(storage, key, value)
+ if (!ok) cacheSet(key, value)
+ return ok
}
function snapshot(value: unknown) {
@@ -148,7 +200,7 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
return {
getItem: (key) => {
const name = item(key)
- const cached = cache.get(name)
+ const cached = cacheGet(name)
if (fallback.disabled && cached !== undefined) return cached
const stored = (() => {
@@ -160,12 +212,12 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
}
})()
if (stored === null) return cached ?? null
- cache.set(name, stored)
+ cacheSet(name, stored)
return stored
},
setItem: (key, value) => {
const name = item(key)
- cache.set(name, value)
+ cacheSet(name, value)
if (fallback.disabled) return
try {
if (write(localStorage, name, value)) return
@@ -177,7 +229,7 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
},
removeItem: (key) => {
const name = item(key)
- cache.delete(name)
+ cacheDelete(name)
if (fallback.disabled) return
try {
localStorage.removeItem(name)
@@ -191,7 +243,7 @@ function localStorageWithPrefix(prefix: string): SyncStorage {
function localStorageDirect(): SyncStorage {
return {
getItem: (key) => {
- const cached = cache.get(key)
+ const cached = cacheGet(key)
if (fallback.disabled && cached !== undefined) return cached
const stored = (() => {
@@ -203,11 +255,11 @@ function localStorageDirect(): SyncStorage {
}
})()
if (stored === null) return cached ?? null
- cache.set(key, stored)
+ cacheSet(key, stored)
return stored
},
setItem: (key, value) => {
- cache.set(key, value)
+ cacheSet(key, value)
if (fallback.disabled) return
try {
if (write(localStorage, key, value)) return
@@ -218,7 +270,7 @@ function localStorageDirect(): SyncStorage {
fallback.disabled = true
},
removeItem: (key) => {
- cache.delete(key)
+ cacheDelete(key)
if (fallback.disabled) return
try {
localStorage.removeItem(key)
diff --git a/packages/app/src/utils/speech.ts b/packages/app/src/utils/speech.ts
index c8acf5241c1..201c1261bd1 100644
--- a/packages/app/src/utils/speech.ts
+++ b/packages/app/src/utils/speech.ts
@@ -78,6 +78,7 @@ export function createSpeechRecognition(opts?: {
let lastInterimSuffix = ""
let shrinkCandidate: string | undefined
let commitTimer: number | undefined
+ let restartTimer: number | undefined
const cancelPendingCommit = () => {
if (commitTimer === undefined) return
@@ -85,6 +86,26 @@ export function createSpeechRecognition(opts?: {
commitTimer = undefined
}
+ const clearRestart = () => {
+ if (restartTimer === undefined) return
+ window.clearTimeout(restartTimer)
+ restartTimer = undefined
+ }
+
+ const scheduleRestart = () => {
+ clearRestart()
+ if (!shouldContinue) return
+ if (!recognition) return
+ restartTimer = window.setTimeout(() => {
+ restartTimer = undefined
+ if (!shouldContinue) return
+ if (!recognition) return
+ try {
+ recognition.start()
+ } catch {}
+ }, 150)
+ }
+
const commitSegment = (segment: string) => {
const nextCommitted = appendSegment(committedText, segment)
if (nextCommitted === committedText) return
@@ -214,17 +235,14 @@ export function createSpeechRecognition(opts?: {
}
recognition.onerror = (e: { error: string }) => {
+ clearRestart()
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
if (e.error === "no-speech" && shouldContinue) {
setStore("interim", "")
if (opts?.onInterim) opts.onInterim("")
- setTimeout(() => {
- try {
- recognition?.start()
- } catch {}
- }, 150)
+ scheduleRestart()
return
}
shouldContinue = false
@@ -232,6 +250,7 @@ export function createSpeechRecognition(opts?: {
}
recognition.onstart = () => {
+ clearRestart()
sessionCommitted = ""
pendingHypothesis = ""
cancelPendingCommit()
@@ -243,22 +262,20 @@ export function createSpeechRecognition(opts?: {
}
recognition.onend = () => {
+ clearRestart()
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
setStore("isRecording", false)
if (shouldContinue) {
- setTimeout(() => {
- try {
- recognition?.start()
- } catch {}
- }, 150)
+ scheduleRestart()
}
}
}
const start = () => {
if (!recognition) return
+ clearRestart()
shouldContinue = true
sessionCommitted = ""
pendingHypothesis = ""
@@ -274,6 +291,7 @@ export function createSpeechRecognition(opts?: {
const stop = () => {
if (!recognition) return
shouldContinue = false
+ clearRestart()
promotePending()
cancelPendingCommit()
lastInterimSuffix = ""
@@ -287,6 +305,7 @@ export function createSpeechRecognition(opts?: {
onCleanup(() => {
shouldContinue = false
+ clearRestart()
promotePending()
cancelPendingCommit()
lastInterimSuffix = ""
diff --git a/packages/app/src/utils/worktree.ts b/packages/app/src/utils/worktree.ts
index 7c0055920b7..581afd5535e 100644
--- a/packages/app/src/utils/worktree.ts
+++ b/packages/app/src/utils/worktree.ts
@@ -13,7 +13,21 @@ type State =
}
const state = new Map()
-const waiters = new Map void>>()
+const waiters = new Map<
+ string,
+ {
+ promise: Promise
+ resolve: (state: State) => void
+ }
+>()
+
+function deferred() {
+ const box = { resolve: (_: State) => {} }
+ const promise = new Promise((resolve) => {
+ box.resolve = resolve
+ })
+ return { promise, resolve: box.resolve }
+}
export const Worktree = {
get(directory: string) {
@@ -27,32 +41,33 @@ export const Worktree = {
},
ready(directory: string) {
const key = normalize(directory)
- state.set(key, { status: "ready" })
- const list = waiters.get(key)
- if (!list) return
+ const next = { status: "ready" } as const
+ state.set(key, next)
+ const waiter = waiters.get(key)
+ if (!waiter) return
waiters.delete(key)
- for (const fn of list) fn({ status: "ready" })
+ waiter.resolve(next)
},
failed(directory: string, message: string) {
const key = normalize(directory)
- state.set(key, { status: "failed", message })
- const list = waiters.get(key)
- if (!list) return
+ const next = { status: "failed", message } as const
+ state.set(key, next)
+ const waiter = waiters.get(key)
+ if (!waiter) return
waiters.delete(key)
- for (const fn of list) fn({ status: "failed", message })
+ waiter.resolve(next)
},
wait(directory: string) {
const key = normalize(directory)
const current = state.get(key)
if (current && current.status !== "pending") return Promise.resolve(current)
- return new Promise((resolve) => {
- const list = waiters.get(key)
- if (!list) {
- waiters.set(key, [resolve])
- return
- }
- list.push(resolve)
- })
+ const existing = waiters.get(key)
+ if (existing) return existing.promise
+
+ const waiter = deferred()
+
+ waiters.set(key, waiter)
+ return waiter.promise
},
}
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index 169f9a7e897..c19c597df19 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
- "version": "1.1.36",
+ "version": "1.1.37",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/console/core/package.json b/packages/console/core/package.json
index 95f019b1cd9..a355b2cf586 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.1.36",
+ "version": "1.1.37",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/console/function/package.json b/packages/console/function/package.json
index 7202c4cfaef..b98859d0be4 100644
--- a/packages/console/function/package.json
+++ b/packages/console/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
- "version": "1.1.36",
+ "version": "1.1.37",
"$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 fc980b6fbfe..0badbf49643 100644
--- a/packages/console/mail/package.json
+++ b/packages/console/mail/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
- "version": "1.1.36",
+ "version": "1.1.37",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index cc6b3af99f7..28a5aa56cb9 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
- "version": "1.1.36",
+ "version": "1.1.37",
"type": "module",
"license": "MIT",
"scripts": {
@@ -15,6 +15,7 @@
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
+ "@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index e086acd9364..dab98f4a006 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/packages/desktop/src-tauri/src/lib.rs
@@ -328,7 +328,15 @@ pub fn run() {
.hidden_title(true);
#[cfg(windows)]
- let window_builder = window_builder.decorations(false);
+ let window_builder = window_builder
+ // Some VPNs set a global/system proxy that WebView2 applies even for loopback
+ // connections, which breaks the app's localhost sidecar server.
+ // Note: when setting additional args, we must re-apply wry's default
+ // `--disable-features=...` flags.
+ .additional_browser_args(
+ "--proxy-bypass-list=<-loopback> --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection",
+ )
+ .decorations(false);
let window = window_builder.build().expect("Failed to create window");
@@ -525,4 +533,4 @@ async fn spawn_local_server(
break Ok(child);
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs
index e3be4a44dab..9ffee8aa5ce 100644
--- a/packages/desktop/src-tauri/src/main.rs
+++ b/packages/desktop/src-tauri/src/main.rs
@@ -52,7 +52,32 @@ fn configure_display_backend() -> Option {
}
fn main() {
- unsafe { std::env::set_var("NO_PROXY", "127.0.0.1,localhost,::1") };
+ // Ensure loopback connections are never sent through proxy settings.
+ // Some VPNs/proxies set HTTP_PROXY/HTTPS_PROXY/ALL_PROXY without excluding localhost.
+ const LOOPBACK: [&str; 3] = ["127.0.0.1", "localhost", "::1"];
+
+ let upsert = |key: &str| {
+ let mut items = std::env::var(key)
+ .unwrap_or_default()
+ .split(',')
+ .map(|v| v.trim())
+ .filter(|v| !v.is_empty())
+ .map(|v| v.to_string())
+ .collect::>();
+
+ for host in LOOPBACK {
+ if items.iter().any(|v| v.eq_ignore_ascii_case(host)) {
+ continue;
+ }
+ items.push(host.to_string());
+ }
+
+ // Safety: called during startup before any threads are spawned.
+ unsafe { std::env::set_var(key, items.join(",")) };
+ };
+
+ upsert("NO_PROXY");
+ upsert("no_proxy");
#[cfg(target_os = "linux")]
{
diff --git a/packages/desktop/src/cli.ts b/packages/desktop/src/cli.ts
index 965ed6ddc00..5a8875cf893 100644
--- a/packages/desktop/src/cli.ts
+++ b/packages/desktop/src/cli.ts
@@ -1,13 +1,15 @@
import { invoke } from "@tauri-apps/api/core"
import { message } from "@tauri-apps/plugin-dialog"
+import { initI18n, t } from "./i18n"
+
export async function installCli(): Promise {
+ await initI18n()
+
try {
const path = await invoke("install_cli")
- await message(`CLI installed to ${path}\n\nRestart your terminal to use the 'opencode' command.`, {
- title: "CLI Installed",
- })
+ await message(t("desktop.cli.installed.message", { path }), { title: t("desktop.cli.installed.title") })
} catch (e) {
- await message(`Failed to install CLI: ${e}`, { title: "Installation Failed" })
+ await message(t("desktop.cli.failed.message", { error: String(e) }), { title: t("desktop.cli.failed.title") })
}
}
diff --git a/packages/desktop/src/i18n/ar.ts b/packages/desktop/src/i18n/ar.ts
new file mode 100644
index 00000000000..c3205cb85e7
--- /dev/null
+++ b/packages/desktop/src/i18n/ar.ts
@@ -0,0 +1,30 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "فشل تشغيل OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "تعذر بدء تشغيل خادم OpenCode المحلي. أعد تشغيل التطبيق، أو تحقق من إعدادات الشبكة (VPN/proxy) وحاول مرة أخرى.",
+}
diff --git a/packages/desktop/src/i18n/br.ts b/packages/desktop/src/i18n/br.ts
new file mode 100644
index 00000000000..8b5a58756d5
--- /dev/null
+++ b/packages/desktop/src/i18n/br.ts
@@ -0,0 +1,31 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "Falha ao iniciar o OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "Não foi possível iniciar o servidor local do OpenCode. Reinicie o aplicativo ou verifique suas configurações de rede (VPN/proxy) e tente novamente.",
+}
diff --git a/packages/desktop/src/i18n/da.ts b/packages/desktop/src/i18n/da.ts
new file mode 100644
index 00000000000..73d47db303b
--- /dev/null
+++ b/packages/desktop/src/i18n/da.ts
@@ -0,0 +1,32 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode kunne ikke starte",
+ "desktop.error.serverStartFailed.description":
+ "Den lokale OpenCode-server kunne ikke startes. Genstart appen, eller tjek dine netværksindstillinger (VPN/proxy) og prøv igen.",
+}
diff --git a/packages/desktop/src/i18n/de.ts b/packages/desktop/src/i18n/de.ts
new file mode 100644
index 00000000000..2559d981e20
--- /dev/null
+++ b/packages/desktop/src/i18n/de.ts
@@ -0,0 +1,32 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode konnte nicht gestartet werden",
+ "desktop.error.serverStartFailed.description":
+ "Der lokale OpenCode-Server konnte nicht gestartet werden. Starten Sie die App neu oder überprüfen Sie Ihre Netzwerkeinstellungen (VPN/Proxy) und versuchen Sie es erneut.",
+}
diff --git a/packages/desktop/src/i18n/en.ts b/packages/desktop/src/i18n/en.ts
new file mode 100644
index 00000000000..c2981f519d8
--- /dev/null
+++ b/packages/desktop/src/i18n/en.ts
@@ -0,0 +1,31 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode failed to start",
+ "desktop.error.serverStartFailed.description":
+ "The local OpenCode server could not be started. Restart the app, or check your network settings (VPN/proxy) and try again.",
+}
diff --git a/packages/desktop/src/i18n/es.ts b/packages/desktop/src/i18n/es.ts
new file mode 100644
index 00000000000..d1045a90cf3
--- /dev/null
+++ b/packages/desktop/src/i18n/es.ts
@@ -0,0 +1,31 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode no pudo iniciarse",
+ "desktop.error.serverStartFailed.description":
+ "No se pudo iniciar el servidor local de OpenCode. Reinicia la aplicación o revisa tu configuración de red (VPN/proxy) y vuelve a intentarlo.",
+}
diff --git a/packages/desktop/src/i18n/fr.ts b/packages/desktop/src/i18n/fr.ts
new file mode 100644
index 00000000000..5c574edf53c
--- /dev/null
+++ b/packages/desktop/src/i18n/fr.ts
@@ -0,0 +1,32 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "Échec du démarrage d'OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "Impossible de démarrer le serveur OpenCode local. Redémarrez l'application ou vérifiez vos paramètres réseau (VPN/proxy) et réessayez.",
+}
diff --git a/packages/desktop/src/i18n/index.ts b/packages/desktop/src/i18n/index.ts
new file mode 100644
index 00000000000..f2496346fcb
--- /dev/null
+++ b/packages/desktop/src/i18n/index.ts
@@ -0,0 +1,147 @@
+import * as i18n from "@solid-primitives/i18n"
+import { Store } from "@tauri-apps/plugin-store"
+
+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 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"
+
+export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
+
+type RawDictionary = typeof appEn & typeof desktopEn
+type Dictionary = i18n.Flatten
+
+const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
+
+function detectLocale(): Locale {
+ if (typeof navigator !== "object") return "en"
+
+ 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"
+ }
+
+ 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
+}
+
+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) }
+ return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) }
+}
+
+const state = {
+ locale: detectLocale(),
+ dict: base as Dictionary,
+ init: undefined as Promise | undefined,
+}
+
+state.dict = build(state.locale)
+
+const translate = i18n.translator(() => state.dict, i18n.resolveTemplate)
+
+export function t(key: keyof Dictionary, params?: Record) {
+ return translate(key, params)
+}
+
+export function initI18n(): Promise {
+ const cached = state.init
+ if (cached) return cached
+
+ const promise = (async () => {
+ const store = await Store.load("opencode.global.dat").catch(() => null)
+ if (!store) return state.locale
+
+ const raw = await store.get("language").catch(() => null)
+ const value = typeof raw === "string" ? JSON.parse(raw) : 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/src/i18n/ja.ts b/packages/desktop/src/i18n/ja.ts
new file mode 100644
index 00000000000..94681ab0699
--- /dev/null
+++ b/packages/desktop/src/i18n/ja.ts
@@ -0,0 +1,32 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode の起動に失敗しました",
+ "desktop.error.serverStartFailed.description":
+ "ローカルの OpenCode サーバーを起動できませんでした。アプリを再起動するか、ネットワーク設定 (VPN/proxy) を確認して再試行してください。",
+}
diff --git a/packages/desktop/src/i18n/ko.ts b/packages/desktop/src/i18n/ko.ts
new file mode 100644
index 00000000000..93136f2ddd0
--- /dev/null
+++ b/packages/desktop/src/i18n/ko.ts
@@ -0,0 +1,31 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode 시작 실패",
+ "desktop.error.serverStartFailed.description":
+ "로컬 OpenCode 서버를 시작할 수 없습니다. 앱을 다시 시작하거나 네트워크 설정(VPN/proxy)을 확인한 후 다시 시도하세요.",
+}
diff --git a/packages/desktop/src/i18n/no.ts b/packages/desktop/src/i18n/no.ts
new file mode 100644
index 00000000000..7deb74687d3
--- /dev/null
+++ b/packages/desktop/src/i18n/no.ts
@@ -0,0 +1,32 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode kunne ikke starte",
+ "desktop.error.serverStartFailed.description":
+ "Den lokale OpenCode-serveren kunne ikke startes. Start appen på nytt, eller sjekk nettverksinnstillingene dine (VPN/proxy) og prøv igjen.",
+}
diff --git a/packages/desktop/src/i18n/pl.ts b/packages/desktop/src/i18n/pl.ts
new file mode 100644
index 00000000000..dac2992ba47
--- /dev/null
+++ b/packages/desktop/src/i18n/pl.ts
@@ -0,0 +1,32 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "Nie udało się uruchomić OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "Nie udało się uruchomić lokalnego serwera OpenCode. Uruchom ponownie aplikację lub sprawdź ustawienia sieciowe (VPN/proxy) i spróbuj ponownie.",
+}
diff --git a/packages/desktop/src/i18n/ru.ts b/packages/desktop/src/i18n/ru.ts
new file mode 100644
index 00000000000..6e34e1aa7da
--- /dev/null
+++ b/packages/desktop/src/i18n/ru.ts
@@ -0,0 +1,31 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "Не удалось запустить OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "Не удалось запустить локальный сервер OpenCode. Перезапустите приложение или проверьте настройки сети (VPN/proxy) и попробуйте снова.",
+}
diff --git a/packages/desktop/src/i18n/zh.ts b/packages/desktop/src/i18n/zh.ts
new file mode 100644
index 00000000000..3f5fe59d40d
--- /dev/null
+++ b/packages/desktop/src/i18n/zh.ts
@@ -0,0 +1,30 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode 启动失败",
+ "desktop.error.serverStartFailed.description":
+ "无法启动本地 OpenCode 服务器。请重启应用,或检查网络设置 (VPN/proxy) 后重试。",
+}
diff --git a/packages/desktop/src/i18n/zht.ts b/packages/desktop/src/i18n/zht.ts
new file mode 100644
index 00000000000..b09bff742c7
--- /dev/null
+++ b/packages/desktop/src/i18n/zht.ts
@@ -0,0 +1,30 @@
+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}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode 啟動失敗",
+ "desktop.error.serverStartFailed.description":
+ "無法啟動本地 OpenCode 伺服器。請重新啟動應用程式,或檢查網路設定 (VPN/proxy) 後再試一次。",
+}
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index b19adfeda5a..344c6be8d9c 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -18,16 +18,17 @@ import { createSignal, Show, Accessor, JSX, createResource, onMount, onCleanup }
import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu"
+import { initI18n, t } from "./i18n"
import pkg from "../package.json"
import "./styles.css"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
- throw new Error(
- "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
- )
+ throw new Error(t("error.dev.rootNotFound"))
}
+void initI18n()
+
// Floating UI can call getComputedStyle with non-elements (e.g., null refs, virtual elements).
// This happens on all platforms (WebView2 on Windows, WKWebView on macOS), not just Windows.
const originalGetComputedStyle = window.getComputedStyle
@@ -54,7 +55,7 @@ const createPlatform = (password: Accessor): Platform => ({
const result = await open({
directory: true,
multiple: opts?.multiple ?? false,
- title: opts?.title ?? "Choose a folder",
+ title: opts?.title ?? t("desktop.dialog.chooseFolder"),
})
return result
},
@@ -63,14 +64,14 @@ const createPlatform = (password: Accessor): Platform => ({
const result = await open({
directory: false,
multiple: opts?.multiple ?? false,
- title: opts?.title ?? "Choose a file",
+ title: opts?.title ?? t("desktop.dialog.chooseFile"),
})
return result
},
async saveFilePickerDialog(opts) {
const result = await save({
- title: opts?.title ?? "Save file",
+ title: opts?.title ?? t("desktop.dialog.saveFile"),
defaultPath: opts?.defaultPath,
})
return result
@@ -380,7 +381,7 @@ function ServerGate(props: { children: (data: Accessor) => JSX.
const errorMessage = () => {
const error = serverData.error
- if (!error) return "Unknown error"
+ if (!error) return t("error.chain.unknown")
if (typeof error === "string") return error
if (error instanceof Error) return error.message
return String(error)
@@ -410,16 +411,15 @@ function ServerGate(props: { children: (data: Accessor) => JSX.
}
>
-
OpenCode failed to start
+
{t("desktop.error.serverStartFailed.title")}
- The local OpenCode server could not be started. Restart the app, or check your network settings (VPN/proxy)
- and try again.
+ {t("desktop.error.serverStartFailed.description")}
diff --git a/packages/desktop/src/menu.ts b/packages/desktop/src/menu.ts
index 1b4c611353a..2edeff42b25 100644
--- a/packages/desktop/src/menu.ts
+++ b/packages/desktop/src/menu.ts
@@ -5,10 +5,13 @@ import { relaunch } from "@tauri-apps/plugin-process"
import { runUpdater, UPDATER_ENABLED } from "./updater"
import { installCli } from "./cli"
+import { initI18n, t } from "./i18n"
export async function createMenu() {
if (ostype() !== "macos") return
+ await initI18n()
+
const menu = await Menu.new({
items: [
await Submenu.new({
@@ -20,22 +23,22 @@ export async function createMenu() {
await MenuItem.new({
enabled: UPDATER_ENABLED,
action: () => runUpdater({ alertOnFail: true }),
- text: "Check For Updates...",
+ text: t("desktop.menu.checkForUpdates"),
}),
await MenuItem.new({
action: () => installCli(),
- text: "Install CLI...",
+ text: t("desktop.menu.installCli"),
}),
await MenuItem.new({
action: async () => window.location.reload(),
- text: "Reload Webview",
+ text: t("desktop.menu.reloadWebview"),
}),
await MenuItem.new({
action: async () => {
await invoke("kill_sidecar").catch(() => undefined)
await relaunch().catch(() => undefined)
},
- text: "Restart",
+ text: t("desktop.menu.restart"),
}),
await PredefinedMenuItem.new({
item: "Separator",
diff --git a/packages/desktop/src/updater.ts b/packages/desktop/src/updater.ts
index 4753ee66390..b48bb6be025 100644
--- a/packages/desktop/src/updater.ts
+++ b/packages/desktop/src/updater.ts
@@ -4,41 +4,45 @@ import { ask, message } from "@tauri-apps/plugin-dialog"
import { invoke } from "@tauri-apps/api/core"
import { type as ostype } from "@tauri-apps/plugin-os"
+import { initI18n, t } from "./i18n"
+
export const UPDATER_ENABLED = window.__OPENCODE__?.updaterEnabled ?? false
export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
+ await initI18n()
+
let update
try {
update = await check()
} catch {
- if (alertOnFail) await message("Failed to check for updates", { title: "Update Check Failed" })
+ if (alertOnFail)
+ await message(t("desktop.updater.checkFailed.message"), { title: t("desktop.updater.checkFailed.title") })
return
}
if (!update) {
- if (alertOnFail)
- await message("You are already using the latest version of OpenCode", { title: "No Update Available" })
+ if (alertOnFail) await message(t("desktop.updater.none.message"), { title: t("desktop.updater.none.title") })
return
}
try {
await update.download()
} catch {
- if (alertOnFail) await message("Failed to download update", { title: "Update Failed" })
+ if (alertOnFail)
+ await message(t("desktop.updater.downloadFailed.message"), { title: t("desktop.updater.downloadFailed.title") })
return
}
- const shouldUpdate = await ask(
- `Version ${update.version} of OpenCode has been downloaded, would you like to install it and relaunch?`,
- { title: "Update Downloaded" },
- )
+ const shouldUpdate = await ask(t("desktop.updater.downloaded.prompt", { version: update.version }), {
+ title: t("desktop.updater.downloaded.title"),
+ })
if (!shouldUpdate) return
try {
if (ostype() === "windows") await invoke("kill_sidecar")
await update.install()
} catch {
- await message("Failed to install update", { title: "Update Failed" })
+ await message(t("desktop.updater.installFailed.message"), { title: t("desktop.updater.installFailed.title") })
return
}
diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json
index 1a216c88f9e..9940d5cf351 100644
--- a/packages/enterprise/package.json
+++ b/packages/enterprise/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
- "version": "1.1.36",
+ "version": "1.1.37",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml
index 7c43d144b9a..f0bdce2c4dd 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.1.36"
+version = "1.1.37"
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.1.36/opencode-darwin-arm64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.36/opencode-darwin-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.36/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/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.1.36/opencode-linux-x64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/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.1.36/opencode-windows-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.37/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
diff --git a/packages/function/package.json b/packages/function/package.json
index 90abb354281..0ee81b74f72 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "1.1.36",
+ "version": "1.1.37",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 68be07e0c14..1168b607f8f 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.1.36",
+ "version": "1.1.37",
"name": "opencode",
"type": "module",
"license": "MIT",
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 10d7a25f88f..f8893a058cc 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -389,10 +389,52 @@ function App() {
slash: {
name: "mcps",
},
+ keybind: "mcp_toggle",
onSelect: () => {
dialog.replace(() => )
},
},
+ ...Array.from({ length: 10 }, (_, i) => {
+ const slot = i + 1
+ return {
+ title: `Toggle MCP slot ${slot}`,
+ value: `mcp.toggle.${slot}`,
+ category: "Agent",
+ keybind: `mcp_toggle_${slot}` as keyof typeof sync.data.config.keybinds,
+ hidden: true,
+ onSelect: async () => {
+ const mcpNames = Object.keys(sync.data.mcp).sort()
+ const mcpName = mcpNames[i]
+ if (!mcpName) {
+ toast.show({
+ variant: "warning",
+ message: `No MCP configured in slot ${slot}`,
+ duration: 2000,
+ })
+ return
+ }
+ try {
+ await local.mcp.toggle(mcpName)
+ const status = await sdk.client.mcp.status()
+ if (status.data) {
+ sync.set("mcp", status.data)
+ }
+ const isEnabled = local.mcp.isEnabled(mcpName)
+ toast.show({
+ variant: "info",
+ message: `${mcpName}: ${isEnabled ? "enabled" : "disabled"}`,
+ duration: 2000,
+ })
+ } catch (error) {
+ toast.show({
+ variant: "error",
+ message: `Failed to toggle ${mcpName}: ${error}`,
+ duration: 3000,
+ })
+ }
+ },
+ }
+ }),
{
title: "Agent cycle",
value: "agent.cycle",
@@ -511,6 +553,7 @@ function App() {
title: "Toggle console",
category: "System",
value: "app.console",
+ keybind: "console_toggle",
onSelect: (dialog) => {
renderer.console.toggle()
dialog.clear()
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
index 4e1171a4201..dc3f337370a 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
@@ -127,7 +127,7 @@ function AutoMethod(props: AutoMethodProps) {
useKeyboard((evt) => {
if (evt.name === "c" && !evt.ctrl && !evt.meta) {
- const code = props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4}/)?.[0] ?? props.authorization.url
+ const code = props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url
Clipboard.copy(code)
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
.catch(toast.error)
diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx
index 771962b75d1..8e6208b140b 100644
--- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx
@@ -1,16 +1,13 @@
import { TextAttributes, RGBA } from "@opentui/core"
import { For, type JSX } from "solid-js"
import { useTheme, tint } from "@tui/context/theme"
+import { logo, marks } from "@/cli/logo"
// Shadow markers (rendered chars in parens):
// _ = full shadow cell (space with bg=shadow)
// ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow)
// ~ = shadow top only (▀ with fg=shadow)
-const SHADOW_MARKER = /[_^~]/
-
-const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█__█ █__█ █^^^ █__█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀`]
-
-const LOGO_RIGHT = [` ▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█___ █__█ █__█ █^^^`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`]
+const SHADOW_MARKER = new RegExp(`[${marks}]`)
export function Logo() {
const { theme } = useTheme()
@@ -75,11 +72,11 @@ export function Logo() {
return (
-
+
{(line, index) => (
{renderLine(line, theme.textMuted, false)}
- {renderLine(LOGO_RIGHT[index()], theme.text, true)}
+ {renderLine(logo.right[index()], theme.text, true)}
)}
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 5f47562d2e3..24994aaaee1 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -58,6 +58,7 @@ import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { Sidebar } from "./sidebar"
+import { Flag } from "@/flag/flag"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import parsers from "../../../../../../parsers-config.ts"
import { Clipboard } from "../../util/clipboard"
@@ -520,6 +521,7 @@ export function Session() {
name: "timestamps",
aliases: ["toggle-timestamps"],
},
+ keybind: "timestamps_toggle",
onSelect: (dialog) => {
setTimestamps((prev) => (prev === "show" ? "hide" : "show"))
dialog.clear()
@@ -533,11 +535,25 @@ export function Session() {
name: "thinking",
aliases: ["toggle-thinking"],
},
+ keybind: "thinking_toggle",
onSelect: (dialog) => {
setShowThinking((prev) => !prev)
dialog.clear()
},
},
+ {
+ title: "Toggle diff wrapping",
+ value: "session.toggle.diffwrap",
+ category: "Session",
+ slash: {
+ name: "diffwrap",
+ },
+ keybind: "diffwrap_toggle",
+ onSelect: (dialog) => {
+ setDiffWrapMode((prev) => (prev === "word" ? "none" : "word"))
+ dialog.clear()
+ },
+ },
{
title: showDetails() ? "Hide tool details" : "Show tool details",
value: "session.toggle.actions",
@@ -1338,15 +1354,22 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
return (
-
+
+
+
+
+
+
+
+
)
diff --git a/packages/opencode/src/cli/logo.ts b/packages/opencode/src/cli/logo.ts
new file mode 100644
index 00000000000..44fb93c15b3
--- /dev/null
+++ b/packages/opencode/src/cli/logo.ts
@@ -0,0 +1,6 @@
+export const logo = {
+ left: [" ", "█▀▀█ █▀▀█ █▀▀█ █▀▀▄", "█__█ █__█ █^^^ █__█", "▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀"],
+ right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"],
+}
+
+export const marks = "_^~"
diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts
index acd1383a070..9df1f4ac550 100644
--- a/packages/opencode/src/cli/ui.ts
+++ b/packages/opencode/src/cli/ui.ts
@@ -1,15 +1,9 @@
import z from "zod"
import { EOL } from "os"
import { NamedError } from "@opencode-ai/util/error"
+import { logo as glyphs } from "./logo"
export namespace UI {
- const LOGO = [
- [` `, ` ▄ `],
- [`█▀▀█ █▀▀█ █▀▀█ █▀▀▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`],
- [`█░░█ █░░█ █▀▀▀ █░░█ `, `█░░░ █░░█ █░░█ █▀▀▀`],
- [`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ `, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`],
- ]
-
export const CancelledError = NamedError.create("UICancelledError", z.void())
export const Style = {
@@ -47,15 +41,50 @@ export namespace UI {
}
export function logo(pad?: string) {
- const result = []
- for (const row of LOGO) {
+ const result: string[] = []
+ const reset = "\x1b[0m"
+ const left = {
+ fg: Bun.color("gray", "ansi") ?? "",
+ shadow: "\x1b[38;5;235m",
+ bg: "\x1b[48;5;235m",
+ }
+ const right = {
+ fg: reset,
+ shadow: "\x1b[38;5;238m",
+ bg: "\x1b[48;5;238m",
+ }
+ const gap = " "
+ const draw = (line: string, fg: string, shadow: string, bg: string) => {
+ const parts: string[] = []
+ for (const char of line) {
+ if (char === "_") {
+ parts.push(bg, " ", reset)
+ continue
+ }
+ if (char === "^") {
+ parts.push(fg, bg, "▀", reset)
+ continue
+ }
+ if (char === "~") {
+ parts.push(shadow, "▀", reset)
+ continue
+ }
+ if (char === " ") {
+ parts.push(" ")
+ continue
+ }
+ parts.push(fg, char, reset)
+ }
+ return parts.join("")
+ }
+ glyphs.left.forEach((row, index) => {
if (pad) result.push(pad)
- result.push(Bun.color("gray", "ansi"))
- result.push(row[0])
- result.push("\x1b[0m")
- result.push(row[1])
+ result.push(draw(row, left.fg, left.shadow, left.bg))
+ result.push(gap)
+ const other = glyphs.right[index] ?? ""
+ result.push(draw(other, right.fg, right.shadow, right.bg))
result.push(EOL)
- }
+ })
return result.join("").trimEnd()
}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 020e626cba8..a70f11e6385 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -650,6 +650,21 @@ export namespace Config {
sidebar_toggle: z.string().optional().default("b").describe("Toggle sidebar"),
scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
+ console_toggle: z.string().optional().default("none").describe("Toggle console"),
+ mcp_toggle: z.string().optional().default("none").describe("Toggle MCP dialog"),
+ mcp_toggle_1: z.string().optional().default("none").describe("Toggle MCP slot 1"),
+ mcp_toggle_2: z.string().optional().default("none").describe("Toggle MCP slot 2"),
+ mcp_toggle_3: z.string().optional().default("none").describe("Toggle MCP slot 3"),
+ mcp_toggle_4: z.string().optional().default("none").describe("Toggle MCP slot 4"),
+ mcp_toggle_5: z.string().optional().default("none").describe("Toggle MCP slot 5"),
+ mcp_toggle_6: z.string().optional().default("none").describe("Toggle MCP slot 6"),
+ mcp_toggle_7: z.string().optional().default("none").describe("Toggle MCP slot 7"),
+ mcp_toggle_8: z.string().optional().default("none").describe("Toggle MCP slot 8"),
+ mcp_toggle_9: z.string().optional().default("none").describe("Toggle MCP slot 9"),
+ mcp_toggle_10: z.string().optional().default("none").describe("Toggle MCP slot 10"),
+ timestamps_toggle: z.string().optional().default("none").describe("Toggle timestamps visibility"),
+ thinking_toggle: z.string().optional().default("none").describe("Toggle thinking visibility"),
+ diffwrap_toggle: z.string().optional().default("none").describe("Toggle diff wrapping"),
status_view: z.string().optional().default("s").describe("View status"),
session_export: z.string().optional().default("x").describe("Export session to editor"),
session_new: z.string().optional().default("n").describe("Create a new session"),
@@ -685,6 +700,7 @@ export namespace Config {
messages_previous: z.string().optional().default("none").describe("Navigate to previous message"),
messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
messages_copy: z.string().optional().default("y").describe("Copy message"),
+ session_copy: z.string().optional().default("none").describe("Copy session transcript"),
messages_undo: z.string().optional().default("u").describe("Undo message"),
messages_redo: z.string().optional().default("r").describe("Redo message"),
messages_toggle_conceal: z
diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts
index 0274dcc82b0..551435e72d0 100644
--- a/packages/opencode/src/flag/flag.ts
+++ b/packages/opencode/src/flag/flag.ts
@@ -46,6 +46,7 @@ export namespace Flag {
export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL")
export const OPENCODE_DISABLE_FILETIME_CHECK = truthy("OPENCODE_DISABLE_FILETIME_CHECK")
export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE")
+ export const OPENCODE_EXPERIMENTAL_MARKDOWN = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_MARKDOWN")
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
function number(key: string) {
diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts
index 198e8ce25d7..b6f1a96a9f5 100644
--- a/packages/opencode/src/plugin/codex.ts
+++ b/packages/opencode/src/plugin/codex.ts
@@ -10,6 +10,7 @@ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
const ISSUER = "https://auth.openai.com"
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
const OAUTH_PORT = 1455
+const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000
interface PkceCodes {
verifier: string
@@ -461,7 +462,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise {
},
methods: [
{
- label: "ChatGPT Pro/Plus",
+ label: "ChatGPT Pro/Plus (browser)",
type: "oauth",
authorize: async () => {
const { redirectUri } = await startOAuthServer()
@@ -490,6 +491,89 @@ export async function CodexAuthPlugin(input: PluginInput): Promise {
}
},
},
+ {
+ label: "ChatGPT Pro/Plus (headless)",
+ type: "oauth",
+ authorize: async () => {
+ const deviceResponse = await fetch(`${ISSUER}/api/accounts/deviceauth/usercode`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "User-Agent": `opencode/${Installation.VERSION}`,
+ },
+ body: JSON.stringify({ client_id: CLIENT_ID }),
+ })
+
+ if (!deviceResponse.ok) throw new Error("Failed to initiate device authorization")
+
+ const deviceData = (await deviceResponse.json()) as {
+ device_auth_id: string
+ user_code: string
+ interval: string
+ }
+ const interval = Math.max(parseInt(deviceData.interval) || 5, 1) * 1000
+
+ return {
+ url: `${ISSUER}/codex/device`,
+ instructions: `Enter code: ${deviceData.user_code}`,
+ method: "auto" as const,
+ async callback() {
+ while (true) {
+ const response = await fetch(`${ISSUER}/api/accounts/deviceauth/token`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "User-Agent": `opencode/${Installation.VERSION}`,
+ },
+ body: JSON.stringify({
+ device_auth_id: deviceData.device_auth_id,
+ user_code: deviceData.user_code,
+ }),
+ })
+
+ if (response.ok) {
+ const data = (await response.json()) as {
+ authorization_code: string
+ code_verifier: string
+ }
+
+ const tokenResponse = await fetch(`${ISSUER}/oauth/token`, {
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
+ body: new URLSearchParams({
+ grant_type: "authorization_code",
+ code: data.authorization_code,
+ redirect_uri: `${ISSUER}/deviceauth/callback`,
+ client_id: CLIENT_ID,
+ code_verifier: data.code_verifier,
+ }).toString(),
+ })
+
+ if (!tokenResponse.ok) {
+ throw new Error(`Token exchange failed: ${tokenResponse.status}`)
+ }
+
+ const tokens: TokenResponse = await tokenResponse.json()
+
+ return {
+ type: "success" as const,
+ refresh: tokens.refresh_token,
+ access: tokens.access_token,
+ expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
+ accountId: extractAccountId(tokens),
+ }
+ }
+
+ if (response.status !== 403 && response.status !== 404) {
+ return { type: "failed" as const }
+ }
+
+ await Bun.sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS)
+ }
+ },
+ }
+ },
+ },
{
label: "Manually enter API Key",
type: "api",
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index 7e8628de7a4..81c14f9d68f 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.1.36",
+ "version": "1.1.37",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json
index e6d968ed626..a752b3362ce 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.1.36",
+ "version": "1.1.37",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 2a63d721215..27f5ccad05b 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -977,6 +977,26 @@ export type KeybindsConfig = {
* Toggle username visibility
*/
username_toggle?: string
+ /**
+ * Toggle console
+ */
+ console_toggle?: string
+ /**
+ * Toggle MCP dialog
+ */
+ mcp_toggle?: string
+ /**
+ * Toggle timestamps visibility
+ */
+ timestamps_toggle?: string
+ /**
+ * Toggle thinking visibility
+ */
+ thinking_toggle?: string
+ /**
+ * Toggle diff wrapping
+ */
+ diffwrap_toggle?: string
/**
* View status
*/
@@ -1085,6 +1105,10 @@ export type KeybindsConfig = {
* Copy message
*/
messages_copy?: string
+ /**
+ * Copy session transcript
+ */
+ session_copy?: string
/**
* Undo message
*/
diff --git a/packages/slack/package.json b/packages/slack/package.json
index 731c2d15d52..81ebec8fe15 100644
--- a/packages/slack/package.json
+++ b/packages/slack/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
- "version": "1.1.36",
+ "version": "1.1.37",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 4c663b1b1c0..27ac2f5cf58 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
- "version": "1.1.36",
+ "version": "1.1.37",
"type": "module",
"license": "MIT",
"exports": {
diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css
index 933c7ce5f24..d9b34592304 100644
--- a/packages/ui/src/components/button.css
+++ b/packages/ui/src/components/button.css
@@ -43,6 +43,10 @@
background-color: transparent;
color: var(--text-strong);
+ [data-slot="icon-svg"] {
+ color: var(--icon-base);
+ }
+
&:hover:not(:disabled) {
background-color: var(--surface-raised-base-hover);
}
@@ -54,8 +58,11 @@
}
&:disabled {
color: var(--text-weak);
- opacity: 0.7;
cursor: not-allowed;
+
+ [data-slot="icon-svg"] {
+ color: var(--icon-disabled);
+ }
}
&[data-selected="true"]:not(:disabled) {
background-color: var(--surface-raised-base-hover);
diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css
index dd75b190524..20d2fef1529 100644
--- a/packages/ui/src/components/session-review.css
+++ b/packages/ui/src/components/session-review.css
@@ -54,6 +54,13 @@
background-color: var(--background-stronger) !important;
}
+ [data-slot="session-review-accordion-item"][data-selected] {
+ [data-slot="session-review-accordion-content"] {
+ box-shadow: var(--shadow-xs-border-select);
+ border-radius: var(--radius-lg);
+ }
+ }
+
[data-slot="accordion-item"] {
[data-slot="accordion-content"] {
display: none;
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 8dfbbb1ca68..84ec934e24d 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/packages/ui/src/components/session-review.tsx
@@ -44,6 +44,7 @@ export interface SessionReviewProps {
comments?: SessionReviewComment[]
focusedComment?: SessionReviewFocus | null
onFocusedCommentChange?: (focus: SessionReviewFocus | null) => void
+ focusedFile?: string
open?: string[]
onOpenChange?: (open: string[]) => void
scrollRef?: (el: HTMLDivElement) => void
@@ -501,6 +502,7 @@ export const SessionReview = (props: SessionReviewProps) => {
id={diffId(diff.file)}
data-file={diff.file}
data-slot="session-review-accordion-item"
+ data-selected={props.focusedFile === diff.file ? "" : undefined}
>
diff --git a/packages/ui/src/components/spinner.css b/packages/ui/src/components/spinner.css
index 6b432d045da..2ca474dc3c3 100644
--- a/packages/ui/src/components/spinner.css
+++ b/packages/ui/src/components/spinner.css
@@ -1,5 +1,5 @@
[data-component="spinner"] {
- color: var(--text-base);
+ color: inherit;
flex-shrink: 0;
width: 18px;
aspect-ratio: 1;
diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css
index 04a9837fd8a..951450d540a 100644
--- a/packages/ui/src/styles/theme.css
+++ b/packages/ui/src/styles/theme.css
@@ -286,6 +286,7 @@
--icon-diff-add-active: var(--mint-light-12);
--icon-diff-delete-base: var(--ember-light-10);
--icon-diff-delete-hover: var(--ember-light-11);
+ --icon-diff-modified-base: var(--icon-warning-base);
--syntax-comment: var(--text-weak);
--syntax-regexp: var(--text-base);
--syntax-string: #006656;
@@ -543,6 +544,7 @@
--icon-diff-add-active: var(--mint-dark-11);
--icon-diff-delete-base: var(--ember-dark-9);
--icon-diff-delete-hover: var(--ember-dark-10);
+ --icon-diff-modified-base: var(--icon-warning-base);
--syntax-comment: var(--text-weak);
--syntax-regexp: var(--text-base);
--syntax-string: #00ceb9;
diff --git a/packages/ui/src/theme/resolve.ts b/packages/ui/src/theme/resolve.ts
index 8c25aefd7b8..2e9c3a52144 100644
--- a/packages/ui/src/theme/resolve.ts
+++ b/packages/ui/src/theme/resolve.ts
@@ -240,6 +240,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
tokens["icon-diff-add-active"] = diffAdd[isDark ? 10 : 11]
tokens["icon-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
tokens["icon-diff-delete-hover"] = diffDelete[isDark ? 9 : 10]
+ tokens["icon-diff-modified-base"] = tokens["icon-warning-base"]
tokens["syntax-comment"] = "var(--text-weak)"
tokens["syntax-regexp"] = "var(--text-base)"
diff --git a/packages/util/package.json b/packages/util/package.json
index 6b9a8d26d54..c59ed182dfd 100644
--- a/packages/util/package.json
+++ b/packages/util/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
- "version": "1.1.36",
+ "version": "1.1.37",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/web/package.json b/packages/web/package.json
index 12b3c689169..3725bbec53b 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.1.36",
+ "version": "1.1.37",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx
index fed64bf77ba..07110dc1b5e 100644
--- a/packages/web/src/content/docs/ecosystem.mdx
+++ b/packages/web/src/content/docs/ecosystem.mdx
@@ -15,37 +15,38 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
## Plugins
-| Name | Description |
-| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
-| [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-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/jamesmurdza/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-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/sdks/vscode/package.json b/sdks/vscode/package.json
index 9812f6bf3a4..7ea0b8979db 100644
--- a/sdks/vscode/package.json
+++ b/sdks/vscode/package.json
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
- "version": "1.1.36",
+ "version": "1.1.37",
"publisher": "sst-dev",
"repository": {
"type": "git",