From 50f208d69f9a3b418290f01f96117308842d9e9d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:17:05 -0600 Subject: [PATCH 001/109] fix(app): suggestion active state broken --- .../components/prompt-input/slash-popover.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/prompt-input/slash-popover.tsx b/packages/app/src/components/prompt-input/slash-popover.tsx index 554a15bb7808..259883d61e84 100644 --- a/packages/app/src/components/prompt-input/slash-popover.tsx +++ b/packages/app/src/components/prompt-input/slash-popover.tsx @@ -53,18 +53,15 @@ export const PromptPopover: Component = (props) => { > {(item) => { - const active = props.atActive === props.atKey(item) - const shared = { - "w-full flex items-center gap-x-2 rounded-md px-2 py-0.5": true, - "bg-surface-raised-base-hover": active, - } + const key = props.atKey(item) if (item.type === "agent") { return ( + + + + + { + props.onDiffRendered?.() + scheduleAnchors() + }} + enableLineSelection={props.onLineComment != null} + onLineSelected={handleLineSelected} + onLineSelectionEnd={handleLineSelectionEnd} + selectedLines={selectedLines()} + commentedLines={commentedLines()} + before={{ + name: diff.file!, + contents: typeof diff.before === "string" ? diff.before : "", }} + after={{ + name: diff.file!, + contents: typeof diff.after === "string" ? diff.after : "", + }} + /> + + + + + {(comment) => ( + setSelection({ file: comment.file, range: comment.selection })} + onClick={() => { + if (isCommentOpen(comment)) { + setOpened(null) + return + } + + openComment(comment) + }} + open={isCommentOpen(comment)} + comment={comment.comment} + selection={selectionLabel(comment.selection)} /> - - )} + )} + + + + {(range) => ( + + setCommenting(null)} + onSubmit={(comment) => { + props.onLineComment?.({ + file: diff.file, + selection: range(), + comment, + preview: selectionPreview(diff, range()), + }) + setCommenting(null) + }} + /> + + )} + diff --git a/packages/ui/src/i18n/ar.ts b/packages/ui/src/i18n/ar.ts index 7ee17e2e0102..9a6c8dcbd050 100644 --- a/packages/ui/src/i18n/ar.ts +++ b/packages/ui/src/i18n/ar.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "مضاف", "ui.sessionReview.change.removed": "محذوف", "ui.sessionReview.change.modified": "معدل", + "ui.sessionReview.image.loading": "جار التحميل...", + "ui.sessionReview.image.placeholder": "صورة", + "ui.sessionReview.largeDiff.title": "Diff كبير جدا لعرضه", + "ui.sessionReview.largeDiff.meta": "الحد: {{lines}} سطر / {{limit}}. الحالي: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "اعرض على أي حال", "ui.lineComment.label.prefix": "تعليق على ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/br.ts b/packages/ui/src/i18n/br.ts index 6d7449d8457d..148b0ae17419 100644 --- a/packages/ui/src/i18n/br.ts +++ b/packages/ui/src/i18n/br.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "Adicionado", "ui.sessionReview.change.removed": "Removido", "ui.sessionReview.change.modified": "Modificado", + "ui.sessionReview.image.loading": "Carregando...", + "ui.sessionReview.image.placeholder": "Imagem", + "ui.sessionReview.largeDiff.title": "Diff grande demais para renderizar", + "ui.sessionReview.largeDiff.meta": "Limite: {{lines}} linhas / {{limit}}. Atual: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Renderizar mesmo assim", "ui.lineComment.label.prefix": "Comentar em ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/bs.ts b/packages/ui/src/i18n/bs.ts index 24e4c12068ee..7614af087f90 100644 --- a/packages/ui/src/i18n/bs.ts +++ b/packages/ui/src/i18n/bs.ts @@ -12,6 +12,11 @@ export const dict = { "ui.sessionReview.change.added": "Dodano", "ui.sessionReview.change.removed": "Uklonjeno", "ui.sessionReview.change.modified": "Izmijenjeno", + "ui.sessionReview.image.loading": "Učitavanje...", + "ui.sessionReview.image.placeholder": "Slika", + "ui.sessionReview.largeDiff.title": "Diff je prevelik za prikaz", + "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} linija / {{limit}}. Trenutno: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Prikaži svejedno", "ui.lineComment.label.prefix": "Komentar na ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/da.ts b/packages/ui/src/i18n/da.ts index 218f3b26a494..2f49a94344cf 100644 --- a/packages/ui/src/i18n/da.ts +++ b/packages/ui/src/i18n/da.ts @@ -9,6 +9,11 @@ export const dict = { "ui.sessionReview.change.added": "Tilføjet", "ui.sessionReview.change.removed": "Fjernet", "ui.sessionReview.change.modified": "Ændret", + "ui.sessionReview.image.loading": "Indlæser...", + "ui.sessionReview.image.placeholder": "Billede", + "ui.sessionReview.largeDiff.title": "Diff er for stor til at blive vist", + "ui.sessionReview.largeDiff.meta": "Grænse: {{lines}} linjer / {{limit}}. Nuværende: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Vis alligevel", "ui.lineComment.label.prefix": "Kommenter på ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Kommenterer på ", diff --git a/packages/ui/src/i18n/de.ts b/packages/ui/src/i18n/de.ts index 921a12c99675..44090b7bdb8c 100644 --- a/packages/ui/src/i18n/de.ts +++ b/packages/ui/src/i18n/de.ts @@ -13,6 +13,11 @@ export const dict = { "ui.sessionReview.change.added": "Hinzugefügt", "ui.sessionReview.change.removed": "Entfernt", "ui.sessionReview.change.modified": "Geändert", + "ui.sessionReview.image.loading": "Wird geladen...", + "ui.sessionReview.image.placeholder": "Bild", + "ui.sessionReview.largeDiff.title": "Diff zu groß zum Rendern", + "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} Zeilen / {{limit}}. Aktuell: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Trotzdem rendern", "ui.lineComment.label.prefix": "Kommentar zu ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Kommentiere ", diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 631bc660a65d..9b6ab0bd6d9e 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "Added", "ui.sessionReview.change.removed": "Removed", "ui.sessionReview.change.modified": "Modified", + "ui.sessionReview.image.loading": "Loading...", + "ui.sessionReview.image.placeholder": "Image", + "ui.sessionReview.largeDiff.title": "Diff too large to render", + "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} lines / {{limit}}. Current: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Render anyway", "ui.lineComment.label.prefix": "Comment on ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/es.ts b/packages/ui/src/i18n/es.ts index 4fd921b606b1..c2f8ac3b9d5c 100644 --- a/packages/ui/src/i18n/es.ts +++ b/packages/ui/src/i18n/es.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "Añadido", "ui.sessionReview.change.removed": "Eliminado", "ui.sessionReview.change.modified": "Modificado", + "ui.sessionReview.image.loading": "Cargando...", + "ui.sessionReview.image.placeholder": "Imagen", + "ui.sessionReview.largeDiff.title": "Diff demasiado grande para renderizar", + "ui.sessionReview.largeDiff.meta": "Límite: {{lines}} líneas / {{limit}}. Actual: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Renderizar de todos modos", "ui.lineComment.label.prefix": "Comentar en ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts index 537d01bba941..679d56fa76ff 100644 --- a/packages/ui/src/i18n/fr.ts +++ b/packages/ui/src/i18n/fr.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "Ajouté", "ui.sessionReview.change.removed": "Supprimé", "ui.sessionReview.change.modified": "Modifié", + "ui.sessionReview.image.loading": "Chargement...", + "ui.sessionReview.image.placeholder": "Image", + "ui.sessionReview.largeDiff.title": "Diff trop volumineux pour être affiché", + "ui.sessionReview.largeDiff.meta": "Limite : {{lines}} lignes / {{limit}}. Actuel : {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Afficher quand même", "ui.lineComment.label.prefix": "Commenter sur ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts index 6086070bdb2c..bf85807d0052 100644 --- a/packages/ui/src/i18n/ja.ts +++ b/packages/ui/src/i18n/ja.ts @@ -9,6 +9,11 @@ export const dict = { "ui.sessionReview.change.added": "追加", "ui.sessionReview.change.removed": "削除", "ui.sessionReview.change.modified": "変更", + "ui.sessionReview.image.loading": "読み込み中...", + "ui.sessionReview.image.placeholder": "画像", + "ui.sessionReview.largeDiff.title": "差分が大きすぎて表示できません", + "ui.sessionReview.largeDiff.meta": "上限: {{lines}} 行 / {{limit}}。現在: {{current}}。", + "ui.sessionReview.largeDiff.renderAnyway": "それでも表示する", "ui.lineComment.label.prefix": "", "ui.lineComment.label.suffix": "へのコメント", "ui.lineComment.editorLabel.prefix": "", diff --git a/packages/ui/src/i18n/ko.ts b/packages/ui/src/i18n/ko.ts index fd394dbb7b52..aba793a11b8d 100644 --- a/packages/ui/src/i18n/ko.ts +++ b/packages/ui/src/i18n/ko.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "추가됨", "ui.sessionReview.change.removed": "삭제됨", "ui.sessionReview.change.modified": "수정됨", + "ui.sessionReview.image.loading": "로딩 중...", + "ui.sessionReview.image.placeholder": "이미지", + "ui.sessionReview.largeDiff.title": "차이가 너무 커서 렌더링할 수 없습니다", + "ui.sessionReview.largeDiff.meta": "제한: {{lines}}줄 / {{limit}}. 현재: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "그래도 렌더링", "ui.lineComment.label.prefix": "", "ui.lineComment.label.suffix": "에 댓글 달기", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index dcb353614d30..7982b3ac75ed 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -11,6 +11,11 @@ export const dict: Record = { "ui.sessionReview.change.added": "Lagt til", "ui.sessionReview.change.removed": "Fjernet", "ui.sessionReview.change.modified": "Endret", + "ui.sessionReview.image.loading": "Laster...", + "ui.sessionReview.image.placeholder": "Bilde", + "ui.sessionReview.largeDiff.title": "Diff er for stor til å gjengi", + "ui.sessionReview.largeDiff.meta": "Grense: {{lines}} linjer / {{limit}}. Nåværende: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Gjengi likevel", "ui.lineComment.label.prefix": "Kommenter på ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts index fb10debbb92d..2489ac7f2ee4 100644 --- a/packages/ui/src/i18n/pl.ts +++ b/packages/ui/src/i18n/pl.ts @@ -9,6 +9,11 @@ export const dict = { "ui.sessionReview.change.added": "Dodano", "ui.sessionReview.change.removed": "Usunięto", "ui.sessionReview.change.modified": "Zmodyfikowano", + "ui.sessionReview.image.loading": "Ładowanie...", + "ui.sessionReview.image.placeholder": "Obraz", + "ui.sessionReview.largeDiff.title": "Diff jest zbyt duży, aby go wyrenderować", + "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} linii / {{limit}}. Obecnie: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Renderuj mimo to", "ui.lineComment.label.prefix": "Komentarz do ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Komentowanie: ", diff --git a/packages/ui/src/i18n/ru.ts b/packages/ui/src/i18n/ru.ts index 417fe0ce8bfe..8e6bb678f249 100644 --- a/packages/ui/src/i18n/ru.ts +++ b/packages/ui/src/i18n/ru.ts @@ -9,6 +9,11 @@ export const dict = { "ui.sessionReview.change.added": "Добавлено", "ui.sessionReview.change.removed": "Удалено", "ui.sessionReview.change.modified": "Изменено", + "ui.sessionReview.image.loading": "Загрузка...", + "ui.sessionReview.image.placeholder": "Изображение", + "ui.sessionReview.largeDiff.title": "Diff слишком большой для отображения", + "ui.sessionReview.largeDiff.meta": "Лимит: {{lines}} строк / {{limit}}. Текущий: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Отобразить всё равно", "ui.lineComment.label.prefix": "Комментарий к ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Комментирование: ", diff --git a/packages/ui/src/i18n/th.ts b/packages/ui/src/i18n/th.ts index 68bb0d733d99..b036eca2e8ae 100644 --- a/packages/ui/src/i18n/th.ts +++ b/packages/ui/src/i18n/th.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "เพิ่ม", "ui.sessionReview.change.removed": "ลบ", "ui.sessionReview.change.modified": "แก้ไข", + "ui.sessionReview.image.loading": "กำลังโหลด...", + "ui.sessionReview.image.placeholder": "รูปภาพ", + "ui.sessionReview.largeDiff.title": "Diff มีขนาดใหญ่เกินไปจนไม่สามารถแสดงผลได้", + "ui.sessionReview.largeDiff.meta": "ขีดจำกัด: {{lines}} บรรทัด / {{limit}}. ปัจจุบัน: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "แสดงผลต่อไป", "ui.lineComment.label.prefix": "แสดงความคิดเห็นบน ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 53beeb1e4f0f..dcb8062a3365 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -12,6 +12,11 @@ export const dict = { "ui.sessionReview.change.added": "已添加", "ui.sessionReview.change.removed": "已移除", "ui.sessionReview.change.modified": "已修改", + "ui.sessionReview.image.loading": "加载中...", + "ui.sessionReview.image.placeholder": "图片", + "ui.sessionReview.largeDiff.title": "差异过大,无法渲染", + "ui.sessionReview.largeDiff.meta": "限制:{{lines}} 行 / {{limit}}。当前:{{current}}。", + "ui.sessionReview.largeDiff.renderAnyway": "仍然渲染", "ui.lineComment.label.prefix": "评论 ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts index 1449b0530ac1..271a6ded3253 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -12,6 +12,11 @@ export const dict = { "ui.sessionReview.change.added": "已新增", "ui.sessionReview.change.removed": "已移除", "ui.sessionReview.change.modified": "已修改", + "ui.sessionReview.image.loading": "載入中...", + "ui.sessionReview.image.placeholder": "圖片", + "ui.sessionReview.largeDiff.title": "差異過大,無法渲染", + "ui.sessionReview.largeDiff.meta": "限制:{{lines}} 行 / {{limit}}。目前:{{current}}。", + "ui.sessionReview.largeDiff.renderAnyway": "仍然渲染", "ui.lineComment.label.prefix": "評論 ", "ui.lineComment.label.suffix": "", diff --git a/packages/util/src/encode.ts b/packages/util/src/encode.ts index 138cf16086df..e4c6e70acb49 100644 --- a/packages/util/src/encode.ts +++ b/packages/util/src/encode.ts @@ -28,3 +28,24 @@ export function checksum(content: string): string | undefined { } return (hash >>> 0).toString(36) } + +export function sampledChecksum(content: string, limit = 500_000): string | undefined { + if (!content) return undefined + if (content.length <= limit) return checksum(content) + + const size = 4096 + const points = [ + 0, + Math.floor(content.length * 0.25), + Math.floor(content.length * 0.5), + Math.floor(content.length * 0.75), + content.length - size, + ] + const hashes = points + .map((point) => { + const start = Math.max(0, Math.min(content.length - size, point - Math.floor(size / 2))) + return checksum(content.slice(start, start + size)) ?? "" + }) + .join(":") + return `${content.length}:${hashes}` +} From 9f20e0d14b1d7db2167b2a81523a2521fe1c3b73 Mon Sep 17 00:00:00 2001 From: Jun <87404676+Seungjun0906@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:12:28 +0900 Subject: [PATCH 041/109] fix(web): sync docs locale cookie on alias redirects (#13109) --- packages/app/src/context/language.tsx | 5 +++++ packages/web/src/middleware.ts | 31 +++++++++++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx index a5d894e62eba..b21ec6d3cc84 100644 --- a/packages/app/src/context/language.tsx +++ b/packages/app/src/context/language.tsx @@ -57,6 +57,10 @@ export type Locale = type RawDictionary = typeof en & typeof uiEn type Dictionary = i18n.Flatten +function cookie(locale: Locale) { + return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` +} + const LOCALES: readonly Locale[] = [ "en", "zh", @@ -199,6 +203,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont createEffect(() => { if (typeof document !== "object") return document.documentElement.lang = locale() + document.cookie = cookie(locale()) }) return { diff --git a/packages/web/src/middleware.ts b/packages/web/src/middleware.ts index 97d085dfbf9d..cf9f97b0b131 100644 --- a/packages/web/src/middleware.ts +++ b/packages/web/src/middleware.ts @@ -12,7 +12,28 @@ function docsAlias(pathname: string) { const next = locale === "root" ? `/docs${tail}` : `/docs/${locale}${tail}` if (next === pathname) return null - return next + return { + path: next, + locale, + } +} + +function cookie(locale: string) { + const value = locale === "root" ? "en" : locale + return `oc_locale=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax` +} + +function redirect(url: URL, path: string, locale?: string) { + const next = new URL(url.toString()) + next.pathname = path + const headers = new Headers({ + Location: next.toString(), + }) + if (locale) headers.set("Set-Cookie", cookie(locale)) + return new Response(null, { + status: 302, + headers, + }) } function localeFromCookie(header: string | null) { @@ -59,9 +80,7 @@ function localeFromAcceptLanguage(header: string | null) { export const onRequest = defineMiddleware((ctx, next) => { const alias = docsAlias(ctx.url.pathname) if (alias) { - const url = new URL(ctx.request.url) - url.pathname = alias - return ctx.redirect(url.toString(), 302) + return redirect(ctx.url, alias.path, alias.locale) } if (ctx.url.pathname !== "/docs" && ctx.url.pathname !== "/docs/") return next() @@ -71,7 +90,5 @@ export const onRequest = defineMiddleware((ctx, next) => { localeFromAcceptLanguage(ctx.request.headers.get("accept-language")) if (!locale || locale === "root") return next() - const url = new URL(ctx.request.url) - url.pathname = `/docs/${locale}/` - return ctx.redirect(url.toString(), 302) + return redirect(ctx.url, `/docs/${locale}/`) }) From ebe5a2b74a564dd92677f2cdaa8d21280aedf7fa Mon Sep 17 00:00:00 2001 From: Chris Yang <18487241+ysm-dev@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:16:14 +0900 Subject: [PATCH 042/109] fix(app): remount SDK/sync tree when server URL changes (#13437) --- packages/app/src/app.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 3032a795f8cd..1121c2e955ac 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,5 +1,5 @@ import "@/index.css" -import { ErrorBoundary, Suspense, lazy, type JSX, type ParentProps } from "solid-js" +import { ErrorBoundary, Show, Suspense, lazy, type JSX, type ParentProps } from "solid-js" import { Router, Route, Navigate } from "@solidjs/router" import { MetaProvider } from "@solidjs/meta" import { Font } from "@opencode-ai/ui/font" @@ -156,8 +156,11 @@ export function AppBaseProviders(props: ParentProps) { function ServerKey(props: ParentProps) { const server = useServer() - if (!server.url) return null - return props.children + return ( + + {props.children} + + ) } export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) { From b1764b2ffdba86c70c6f2777d1342ad87ac6ec41 Mon Sep 17 00:00:00 2001 From: Annopick Date: Fri, 13 Feb 2026 19:18:47 +0800 Subject: [PATCH 043/109] docs: Fix zh-cn translation mistake in tools.mdx (#13407) --- packages/web/src/content/docs/zh-cn/tools.mdx | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/packages/web/src/content/docs/zh-cn/tools.mdx b/packages/web/src/content/docs/zh-cn/tools.mdx index 1be9d669012c..a1a97a3ed71e 100644 --- a/packages/web/src/content/docs/zh-cn/tools.mdx +++ b/packages/web/src/content/docs/zh-cn/tools.mdx @@ -24,7 +24,7 @@ Tools allow the LLM to perform actions in your codebase. opencode comes with a s } ``` -您还可以使用万用字元同时控制多个工具。例如,要求 MCP 服务器批准所有工具: +您还可以使用通配符同时控制多个工具。例如,要求 MCP 服务器批准所有工具: ```json title="opencode.json" { @@ -39,15 +39,15 @@ Tools allow the LLM to perform actions in your codebase. opencode comes with a s --- -## 內建 +## 內建工具 以下是 opencode 中可用的所有内置工具。 --- -### 巴什 +### Bash -在专案环境中执行shell命令。 +在专项任务环境中执行shell命令。 ```json title="opencode.json" {4} { @@ -58,13 +58,13 @@ Tools allow the LLM to perform actions in your codebase. opencode comes with a s } ``` -This tool allows the LLM to run terminal commands like `npm install`, `git status`, or any other shell command. +这个工具允许 LLM 运行终端命令,例如:`npm install`, `git status`,或者其他任何终端命令。 --- -### 編輯 +### 编辑 -使用精確的字符串替換修改現有文件。 +使用精确的字符串替换来修改现有文件。 ```json title="opencode.json" {4} { @@ -75,13 +75,13 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -该工具取消替换精确的文字来匹配对文件执行精确编辑。这是 LLM 修改代码的主要方式。 +该工具通过替换完全匹配的文本来对文件进行精确编辑。这是 LLM 修改代码的主要方式。 --- -### 寫 +### 写入 -建立新文件或覆盖現有文件。 +创建新文件或覆盖现有文件。 ```json title="opencode.json" {4} { @@ -92,17 +92,17 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -使用它允许 LLM 创建新文件。如果现有文件已经存在,将会覆盖它们。 +使用此功能可允许 LLM 创建新文件。如果文件已存在,则会覆盖现有文件。 :::note -`write`工具由`edit`许可权控制,该许可权主题所有文件修改(`edit`、`write`、`patch`、`multiedit`)。 +`写入`工具由`编辑`权限控制,涵盖所有文件修改(`编辑`、`写入`、`修补`、`多重编辑`)。 ::: --- -### 讀 +### 读取 -從程式碼庫中讀取文件內容。 +读取代码库中的文件内容。 ```json title="opencode.json" {4} { @@ -113,13 +113,13 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -该工具讀取文件并返回其內容。它支持讀取大文件的特定行范围。 +该工具读取文件并返回其内容。它支持读取大型文件中的特定行范围。 --- ### grep -使用正規表示式搜索文件內容。 +使用正则表达式搜索文件内容。 ```json title="opencode.json" {4} { @@ -130,13 +130,13 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -在您的程式碼庫中快速進行內容搜索。支持完整的正規表示式語法和文件模式过濾。 +快速搜索代码库中的内容。支持完整的正则表达式语法和文件模式过滤。 --- -### 全域性 +### 通配符 -通过模式匹配查询文件。 +通过模式匹配查找文件。 ```json title="opencode.json" {4} { @@ -147,13 +147,13 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -使用 `**/*.js` 或 `src/**/*.ts` 等全域性模式搜索档案。返回按时间排序的匹配档案路径修改。 +使用类似 **/*.js 或 src/**/*.ts 的通配符模式搜索文件。返回按修改时间排序的匹配文件路径。 --- -### 列表 +### 罗列 -列出給定路徑中的文件和目录。 +列出给定路径下的文件和目录。 ```json title="opencode.json" {4} { @@ -164,16 +164,16 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -该工具列出目录內容。它接受全域性模式來过濾結果。 +此工具用于列出目录内容。它接受通配符模式来筛选结果。 --- ### lsp(实验性) -与您配置的LSP服务器交互,通知计划码智慧功能,例如定义、引用、悬停资讯和呼叫层次结构。 +与已配置的 LSP 服务器交互,以获取代码智能功能,例如定义、引用、悬停信息和调用层次结构。 :::note -This tool is only available when `OPENCODE_EXPERIMENTAL_LSP_TOOL=true` (or `OPENCODE_EXPERIMENTAL=true`). +只有当 OPENCODE_EXPERIMENTAL_LSP_TOOL=true(或 OPENCODE_EXPERIMENTAL=true)时,此工具才可用。 ::: ```json title="opencode.json" {4} @@ -187,13 +187,13 @@ This tool is only available when `OPENCODE_EXPERIMENTAL_LSP_TOOL=true` (or `OPEN 支持的操作包括 `goToDefinition`、`findReferences`、`hover`、`documentSymbol`、`workspaceSymbol`、`goToImplementation`、`prepareCallHierarchy`、`incomingCalls` 和 `outgoingCalls`。 -To configure which LSP servers are available for your project, see [LSP Servers](/docs/lsp). +要配置哪些 LSP 服务器可用于您的项目,请参阅 [LSP Servers](/docs/lsp). --- -### 修補 +### 修补 -对文件应用補丁。 +对文件应用补丁。 ```json title="opencode.json" {4} { @@ -204,17 +204,17 @@ To configure which LSP servers are available for your project, see [LSP Servers] } ``` -该工具将補丁文件应用到您的程式碼庫。对于应用來自各種來源的差異和補丁很有帮助。 +此工具可将补丁文件应用到您的代码库。它可用于应用来自各种来源的差异和补丁。 :::note -`patch`工具由`edit`许可权控制,该许可权主题所有文件修改(`edit`、`write`、`patch`、`multiedit`)。 +`修补`工具由`编辑`权限控制,涵盖所有文件修改(`编辑`、`写入`、`修补`、`多重编辑`)。 ::: --- ### 技能 -加载[skill](/docs/skills)(`SKILL.md` 档案)并在对话中返回其内容。 +加载[技能](/docs/skills)(`SKILL.md` 文件)并在对话中返回其内容。 ```json title="opencode.json" {4} { @@ -227,9 +227,9 @@ To configure which LSP servers are available for your project, see [LSP Servers] --- -### 待辦寫入 +### 写入待办 -在編碼会话期間管理待辦事項列表。 +在编码会话过程中管理待办事项列表。 ```json title="opencode.json" {4} { @@ -240,17 +240,17 @@ To configure which LSP servers are available for your project, see [LSP Servers] } ``` -建立和更新任务列表以跟踪复杂操作期间的详细信息。LLM 使用它来组织多步骤任务。 +创建和更新任务列表,以跟踪复杂操作的进度。LLM 利用此功能来组织多步骤任务。 :::note -默认情况下,子代理取消此工具,但您可以手动启用它。 [了解更多](/docs/agents/#permissions) +此工具默认情况下对子代理禁用,但您可以手动启用它。 [了解更多](/docs/agents/#permissions) ::: --- -### 託多雷德 +### 读取待办 -閱讀現有的待辦事項列表。 +阅读现有的待办事项清单。 ```json title="opencode.json" {4} { @@ -261,17 +261,17 @@ To configure which LSP servers are available for your project, see [LSP Servers] } ``` -读取当前完成待办事项列表状态。由 LLM 用于跟踪哪些任务待处理或已已。 +读取当前待办事项列表状态。LLM 使用此信息来跟踪哪些任务处于待处理状态或已完成状态。 :::note -默认情况下,子代理取消此工具,但您可以手动启用它。 [了解更多](/docs/agents/#permissions) +此工具默认情况下对子代理禁用,但您可以手动启用它。 [了解更多](/docs/agents/#permissions) ::: --- -### 網頁抓取 +### 网页获取 -获取網頁內容。 +获取网页内容。 ```json title="opencode.json" {4} { @@ -282,18 +282,18 @@ To configure which LSP servers are available for your project, see [LSP Servers] } ``` -允许 LLM 获取和读取网页。对于查询文件或研究线上资源很有帮助。 +允许LLM获取并读取网页。可用于查找文档或研究在线资源。 --- -### 網路搜索 +### 网页搜索 -在網路上搜索資訊。 +在网上搜索信息。 :::note -仅当使用 opencode 提供或 `OPENCODE_ENABLE_EXA` 程序环境变量设置为任何真值(例如 `true` 或 `1`)时,此工具才可用。 +只有在使用 OpenCode 提供程序时,或者当 OPENCODE_ENABLE_EXA 环境变量被设置为任何真值(例如 true 或 1)时,此工具才可用。 -要在启动 opencode 时启用: +在启动 OpenCode 时启用: ```bash OPENCODE_ENABLE_EXA=1 opencode @@ -310,19 +310,19 @@ OPENCODE_ENABLE_EXA=1 opencode } ``` -使用 Exa AI 执行网路搜索以线上查询相关资讯。对于研究主题、查询时事或收集训练超出数据范围的资讯很有帮助。 +利用 Exa AI 进行网络搜索,查找相关信息。可用于研究特定主题、了解时事新闻或收集超出训练数据范围的信息。 -不需要 API 密钥 — 该工具消耗身份验证即可直接连线到 Exa AI 的托管 MCP 服务。 +无需 API 密钥——该工具无需身份验证即可直接连接到 Exa AI 托管的 MCP 服务。 :::tip -当您需要查询资讯(发现)时,请使用 `websearch`;当您需要从特定 URL 检索内容(搜索)时,请使用 `webfetch`。 +当您需要查找信息时,请使用`网页搜索`;当您需要从特定 URL 检索内容时,请使用`网页获取`。 ::: --- -### 問題 +### 提问 -在执行过程中詢問用户問題。 +在执行过程中向用户提问。 ```json title="opencode.json" {4} { @@ -333,20 +333,20 @@ OPENCODE_ENABLE_EXA=1 opencode } ``` -该工具允许 LLM 在任务期间询问用户问题。它适用于: +该工具允许 LLM 在执行任务期间向用户提问。它在以下方面很有用: -- 收集用户偏好或要求 -- 澄清不明確的指令 -- 就實施选择做出決策 -- 提供选择方向 +- 收集用户偏好或需求 +- 澄清含糊不清的指示 +- 就实施方案做出决定 +- 提供关于选择下一步方向的选项 -每个問題都包含標題、問題文字和選項列表。用户可以從提供的選項中進行选择或輸入自定義答案。当存在多个問題時,用户可以在提交所有答案之前在这些問題之间导航。 +每个问题都包含标题、问题正文和选项列表。用户可以从提供的选项中选择答案,也可以输入自定义答案。如果有多个问题,用户可以在提交所有答案之前在不同问题之间切换。 --- -## 定製工具 +## 自定义工具 -自定义工具可以让您定义LLM可以调用自己的函式。这些是在您的配置文件中定义的并且可以执行任何代码。 +自定义工具允许您定义LLM可以调用的自定义函数。这些函数在您的配置文件中定义,并且可以执行任意代码。 [了解更多](/docs/custom-tools)关于创建自定义工具。 @@ -360,15 +360,15 @@ MCP(模型上下文协议)服务器允许您集成外部工具和服务。 --- -## 内部結構 +## 内部规则 -Internally, tools like `grep`, `glob`, and `list` use [ripgrep](https://github.com/BurntSushi/ripgrep) under the hood. By default, ripgrep respects `.gitignore` patterns, which means files and directories listed in your `.gitignore` will be excluded from searches and listings. +在内部,`grep`、 `通配符` 和 `罗列` 等工具底层都使用了 ripgrep。默认情况下,ripgrep 会遵循 .gitignore 文件中的规则,这意味着 .gitignore 文件中列出的文件和目录将被排除在搜索和列表之外。 --- ### 忽略模式 -要包含通常会被忽略的文件,请在专案根目录中建立 `.ignore` 文件。该文件可以明确允许某些路径。 +为了使工具不跳过那些通常会被忽略的文件,请在项目根目录下创建一个 `.ignore` 文件。该文件内定义的目录可以不会被跳过。 ```text title=".ignore" !node_modules/ @@ -376,4 +376,4 @@ Internally, tools like `grep`, `glob`, and `list` use [ripgrep](https://github.c !build/ ``` -例如,此 `.ignore` 档案允许 ripgrep 在 `node_modules/`、`dist/` 和 `build/` 目录中搜索,即使它们列在 `.gitignore` 中。 +例如,这个 `.ignore` 文件允许 ripgrep 在 `node_modules/`、`dist/` 和 `build/` 目录中搜索,即使它们已在 `.gitignore` 中列出。 From f991a6c0b6bba97be27f3c132c14c5fa78d05536 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 13 Feb 2026 11:19:37 +0000 Subject: [PATCH 044/109] chore: generate --- packages/web/src/content/docs/zh-cn/tools.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/zh-cn/tools.mdx b/packages/web/src/content/docs/zh-cn/tools.mdx index a1a97a3ed71e..86190a4e06ee 100644 --- a/packages/web/src/content/docs/zh-cn/tools.mdx +++ b/packages/web/src/content/docs/zh-cn/tools.mdx @@ -147,7 +147,7 @@ Tools allow the LLM to perform actions in your codebase. opencode comes with a s } ``` -使用类似 **/*.js 或 src/**/*.ts 的通配符模式搜索文件。返回按修改时间排序的匹配文件路径。 +使用类似 **/\*.js 或 src/**/\*.ts 的通配符模式搜索文件。返回按修改时间排序的匹配文件路径。 --- From e242fe19e48f6aa70e5c3f7d54f34d688181edb2 Mon Sep 17 00:00:00 2001 From: eytans Date: Fri, 13 Feb 2026 13:25:47 +0200 Subject: [PATCH 045/109] fix(web): use prompt_async endpoint to avoid timeout over VPN/tunnel (#12749) --- packages/app/e2e/prompt/prompt-async.spec.ts | 43 +++++++++++++++++++ .../app/src/components/prompt-input/submit.ts | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 packages/app/e2e/prompt/prompt-async.spec.ts diff --git a/packages/app/e2e/prompt/prompt-async.spec.ts b/packages/app/e2e/prompt/prompt-async.spec.ts new file mode 100644 index 000000000000..ce9b1a7a3bb1 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-async.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { sessionIDFromUrl } from "../actions" + +// Regression test for Issue #12453: the synchronous POST /message endpoint holds +// the connection open while the agent works, causing "Failed to fetch" over +// VPN/Tailscale. The fix switches to POST /prompt_async which returns immediately. +test("prompt succeeds when sync message endpoint is unreachable", async ({ page, sdk, gotoSession }) => { + test.setTimeout(120_000) + + // Simulate Tailscale/VPN killing the long-lived sync connection + await page.route("**/session/*/message", (route) => route.abort("connectionfailed")) + + await gotoSession() + + const token = `E2E_ASYNC_${Date.now()}` + await page.locator(promptSelector).click() + await page.keyboard.type(`Reply with exactly: ${token}`) + await page.keyboard.press("Enter") + + await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 }) + const sessionID = sessionIDFromUrl(page.url())! + + try { + // Agent response arrives via SSE despite sync endpoint being dead + await expect + .poll( + async () => { + const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? []) + return messages + .filter((m) => m.info.role === "assistant") + .flatMap((m) => m.parts) + .filter((p) => p.type === "text") + .map((p) => p.text) + .join("\n") + }, + { timeout: 90_000 }, + ) + .toContain(token) + } finally { + await sdk.session.delete({ sessionID }).catch(() => undefined) + } +}) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 49d75a95ecc7..9a1fba5d5c49 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -385,7 +385,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { const send = async () => { const ok = await waitForWorktree() if (!ok) return - await client.session.prompt({ + await client.session.promptAsync({ sessionID: session.id, agent, model, From 1c71604e0a2a34786daa99b7002c2f567671051a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 13 Feb 2026 05:50:12 -0600 Subject: [PATCH 046/109] fix(app): terminal resize --- packages/app/src/components/terminal.tsx | 138 ++++++++++++++++------- 1 file changed, 98 insertions(+), 40 deletions(-) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index ccf7012d20ab..14413dfda677 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -156,6 +156,10 @@ export const Terminal = (props: TerminalProps) => { let serializeAddon: SerializeAddon let fitAddon: FitAddon let handleResize: () => void + let fitFrame: number | undefined + let sizeTimer: ReturnType | undefined + let pendingSize: { cols: number; rows: number } | undefined + let lastSize: { cols: number; rows: number } | undefined let disposed = false const cleanups: VoidFunction[] = [] const start = @@ -209,6 +213,43 @@ export const Terminal = (props: TerminalProps) => { const [terminalColors, setTerminalColors] = createSignal(getTerminalColors()) + const scheduleFit = () => { + if (disposed) return + if (!fitAddon) return + if (fitFrame !== undefined) return + + fitFrame = requestAnimationFrame(() => { + fitFrame = undefined + if (disposed) return + fitAddon.fit() + }) + } + + const scheduleSize = (cols: number, rows: number) => { + if (disposed) return + if (lastSize?.cols === cols && lastSize?.rows === rows) return + + pendingSize = { cols, rows } + + if (!lastSize) { + lastSize = pendingSize + void pushSize(cols, rows) + return + } + + if (sizeTimer !== undefined) return + sizeTimer = setTimeout(() => { + sizeTimer = undefined + const next = pendingSize + if (!next) return + pendingSize = undefined + if (disposed) return + if (lastSize?.cols === next.cols && lastSize?.rows === next.rows) return + lastSize = next + void pushSize(next.cols, next.rows) + }, 100) + } + createEffect(() => { const colors = getTerminalColors() setTerminalColors(colors) @@ -220,6 +261,16 @@ export const Terminal = (props: TerminalProps) => { const font = monoFontFamily(settings.appearance.font()) if (!term) return setOptionIfSupported(term, "fontFamily", font) + scheduleFit() + }) + + let zoom = platform.webviewZoom?.() + createEffect(() => { + const next = platform.webviewZoom?.() + if (next === undefined) return + if (next === zoom) return + zoom = next + scheduleFit() }) const focusTerminal = () => { @@ -263,25 +314,6 @@ export const Terminal = (props: TerminalProps) => { const once = { value: false } - const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`) - url.searchParams.set("directory", sdk.directory) - url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0)) - url.protocol = url.protocol === "https:" ? "wss:" : "ws:" - if (window.__OPENCODE__?.serverPassword) { - url.username = "opencode" - url.password = window.__OPENCODE__?.serverPassword - } - const socket = new WebSocket(url) - socket.binaryType = "arraybuffer" - cleanups.push(() => { - if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close() - }) - if (disposed) { - cleanup() - return - } - ws = socket - const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" const restoreSize = restore && @@ -344,9 +376,28 @@ export const Terminal = (props: TerminalProps) => { focusTerminal() + if (typeof document !== "undefined" && document.fonts) { + document.fonts.ready.then(scheduleFit) + } + + const onResize = t.onResize((size) => { + scheduleSize(size.cols, size.rows) + }) + cleanups.push(() => disposeIfDisposable(onResize)) + const onData = t.onData((data) => { + if (ws?.readyState === WebSocket.OPEN) ws.send(data) + }) + cleanups.push(() => disposeIfDisposable(onData)) + const onKey = t.onKey((key) => { + if (key.key == "Enter") { + props.onSubmit?.() + } + }) + cleanups.push(() => disposeIfDisposable(onKey)) + const startResize = () => { fit.observeResize() - handleResize = () => fit.fit() + handleResize = scheduleFit window.addEventListener("resize", handleResize) cleanups.push(() => window.removeEventListener("resize", handleResize)) } @@ -354,11 +405,13 @@ export const Terminal = (props: TerminalProps) => { if (restore && restoreSize) { t.write(restore, () => { fit.fit() + scheduleSize(t.cols, t.rows) if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) startResize() }) } else { fit.fit() + scheduleSize(t.cols, t.rows) if (restore) { t.write(restore, () => { if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) @@ -367,35 +420,38 @@ export const Terminal = (props: TerminalProps) => { startResize() } - const onResize = t.onResize(async (size) => { - if (socket.readyState === WebSocket.OPEN) { - await pushSize(size.cols, size.rows) - } - }) - cleanups.push(() => disposeIfDisposable(onResize)) - const onData = t.onData((data) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(data) - } - }) - cleanups.push(() => disposeIfDisposable(onData)) - const onKey = t.onKey((key) => { - if (key.key == "Enter") { - props.onSubmit?.() - } - }) - cleanups.push(() => disposeIfDisposable(onKey)) // t.onScroll((ydisp) => { // console.log("Scroll position:", ydisp) // }) + const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`) + url.searchParams.set("directory", sdk.directory) + url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0)) + url.protocol = url.protocol === "https:" ? "wss:" : "ws:" + if (window.__OPENCODE__?.serverPassword) { + url.username = "opencode" + url.password = window.__OPENCODE__?.serverPassword + } + const socket = new WebSocket(url) + socket.binaryType = "arraybuffer" + ws = socket + cleanups.push(() => { + if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close() + }) + if (disposed) { + cleanup() + return + } + const handleOpen = () => { local.onConnect?.() - void pushSize(t.cols, t.rows) + scheduleSize(t.cols, t.rows) } socket.addEventListener("open", handleOpen) cleanups.push(() => socket.removeEventListener("open", handleOpen)) + if (socket.readyState === WebSocket.OPEN) handleOpen() + const decoder = new TextDecoder() const handleMessage = (event: MessageEvent) => { @@ -462,6 +518,8 @@ export const Terminal = (props: TerminalProps) => { onCleanup(() => { disposed = true + if (fitFrame !== undefined) cancelAnimationFrame(fitFrame) + if (sizeTimer !== undefined) clearTimeout(sizeTimer) output?.flush() persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup }) cleanup() @@ -477,7 +535,7 @@ export const Terminal = (props: TerminalProps) => { classList={{ ...(local.classList ?? {}), "select-text": true, - "size-full px-6 py-3 font-mono": true, + "size-full px-6 py-3 font-mono relative overflow-hidden": true, [local.class ?? ""]: !!local.class, }} {...others} From 4f51c0912d76698325862e8fcd7d484b7b9a61fe Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 13 Feb 2026 05:52:33 -0600 Subject: [PATCH 047/109] chore: cleanup --- packages/app/src/components/session/session-header.tsx | 2 +- packages/app/src/pages/session.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index b85b9a536a9d..f81a2ec44031 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -552,7 +552,7 @@ export function SessionHeader() { -