diff --git a/packages/app/src/components/prompt-input/editor-dom.ts b/packages/app/src/components/prompt-input/editor-dom.ts
index 8575140d..1f248b82 100644
--- a/packages/app/src/components/prompt-input/editor-dom.ts
+++ b/packages/app/src/components/prompt-input/editor-dom.ts
@@ -79,16 +79,19 @@ export function setCursorPosition(parent: HTMLElement, position: number) {
const selection = window.getSelection()
if (remaining === 0) {
range.setStartBefore(node)
+ range.collapse(true)
+ selection?.removeAllRanges()
+ selection?.addRange(range)
+ return
}
- if (remaining > 0 && isPill) {
+ if (isPill) {
range.setStartAfter(node)
}
- if (remaining > 0 && isBreak) {
+ if (isBreak) {
const next = node.nextSibling
if (next && next.nodeType === Node.TEXT_NODE) {
range.setStart(next, 0)
- }
- if (!next || next.nodeType !== Node.TEXT_NODE) {
+ } else {
range.setStartAfter(node)
}
}
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 8a2fbf87..f5b4c9ee 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -168,9 +168,9 @@ export const dict = {
"provider.custom.field.name.placeholder": "My AI Provider",
"provider.custom.field.baseURL.label": "Base URL",
"provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
- "provider.custom.field.apiKey.label": "API key",
- "provider.custom.field.apiKey.placeholder": "API key",
- "provider.custom.field.apiKey.description": "Optional. Leave empty if you manage auth via headers.",
+ "provider.custom.field.apiKey.label": "API key (optional)",
+ "provider.custom.field.apiKey.placeholder": "sk-... or leave empty for local providers",
+ "provider.custom.field.apiKey.description": "Optional. Leave empty for local providers or if you manage auth via headers.",
"provider.custom.models.label": "Models",
"provider.custom.models.id.label": "ID",
"provider.custom.models.id.placeholder": "model-id",
diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts
index d95bfd19..15e7f108 100644
--- a/packages/app/src/i18n/zh.ts
+++ b/packages/app/src/i18n/zh.ts
@@ -188,9 +188,9 @@ export const dict = {
"provider.custom.field.name.placeholder": "我的 AI 提供商",
"provider.custom.field.baseURL.label": "基础 URL",
"provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
- "provider.custom.field.apiKey.label": "API 密钥",
- "provider.custom.field.apiKey.placeholder": "API 密钥",
- "provider.custom.field.apiKey.description": "可选。如果你通过请求头管理认证,可留空。",
+ "provider.custom.field.apiKey.label": "API 密钥(可选)",
+ "provider.custom.field.apiKey.placeholder": "sk-... 或本地模型留空",
+ "provider.custom.field.apiKey.description": "可选。本地模型或通过请求头管理认证时可留空。",
"provider.custom.models.label": "模型",
"provider.custom.models.id.label": "ID",
"provider.custom.models.id.placeholder": "model-id",
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-mimo-login.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-mimo-login.tsx
index 0c92e7f4..ded8e65e 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-mimo-login.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-mimo-login.tsx
@@ -197,7 +197,7 @@ function MimoOAuthFlow(props: { url: string; instructions: string }) {
{props.url}
- {props.instructions}
+ {t("tui.dialog.login.flow.instructions")}
{t("tui.dialog.login.flow.waiting")}
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
index a7d1a248..a6f55cb0 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
@@ -200,7 +200,6 @@ export function DialogModel(props: { providerID?: string }) {
]}
onFilter={setQuery}
flat={true}
- skipFilter={true}
title={title()}
current={local.model.current()}
/>
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index d04725fa..d34fc35c 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -1541,7 +1541,11 @@ export function Prompt(props: PromptProps) {
// Normalize line endings at the boundary
// Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste
// Replace CRLF first, then any remaining CR
- const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n")
+ const normalizedText = decodePasteBytes(event.bytes)
+ // Some terminals prepend a literal "Paste" label before bracketed paste data
+ .replace(/^Paste\r?\n?/, "")
+ .replace(/\r\n/g, "\n")
+ .replace(/\r/g, "\n")
// Windows Terminal <1.25 can surface image-only clipboard as an
// empty bracketed paste. Windows Terminal 1.25+ does not.
@@ -1686,7 +1690,8 @@ export function Prompt(props: PromptProps) {
{(() => {
const busyMessage = createMemo(() => {
const s = status()
- return s.type === "busy" ? s.message : undefined
+ if (s.type !== "busy" || !s.message) return undefined
+ return s.message.length > 60 ? s.message.slice(0, 60) + "..." : s.message
})
return (
diff --git a/packages/opencode/src/cli/cmd/tui/i18n/en.ts b/packages/opencode/src/cli/cmd/tui/i18n/en.ts
index 81c4b679..83e553a7 100644
--- a/packages/opencode/src/cli/cmd/tui/i18n/en.ts
+++ b/packages/opencode/src/cli/cmd/tui/i18n/en.ts
@@ -383,6 +383,7 @@ export const dict: Record = {
"tui.dialog.login.flow.manual_hint": "Browser didn't open? Visit manually:",
"tui.dialog.login.flow.waiting": "Waiting for browser authorization...",
"tui.dialog.login.flow.invalid_code": "Invalid Code, please retry",
+ "tui.dialog.login.flow.instructions": "Complete authorization in the browser, or paste the Code to finish login.",
// Question i18n — plan_exit
"tui.question.plan_exit.question": "Plan at {{plan}} is complete. Would you like to switch to the build agent and start implementing?",
diff --git a/packages/opencode/src/cli/cmd/tui/i18n/zh.ts b/packages/opencode/src/cli/cmd/tui/i18n/zh.ts
index 68b3f151..fc274091 100644
--- a/packages/opencode/src/cli/cmd/tui/i18n/zh.ts
+++ b/packages/opencode/src/cli/cmd/tui/i18n/zh.ts
@@ -376,6 +376,7 @@ export const dict = {
"tui.dialog.login.flow.manual_hint": "浏览器未打开?手动访问:",
"tui.dialog.login.flow.waiting": "等待浏览器授权中...",
"tui.dialog.login.flow.invalid_code": "Code 无效,请重试",
+ "tui.dialog.login.flow.instructions": "在浏览器中完成授权,或粘贴 Code 完成登录。",
// Question i18n — plan_exit
"tui.question.plan_exit.question": "{{plan}} 处的计划已完成。是否切换到 build 智能体开始实现?",
diff --git a/packages/opencode/src/plugin/mimo.ts b/packages/opencode/src/plugin/mimo.ts
index bfdd52bc..a356685d 100644
--- a/packages/opencode/src/plugin/mimo.ts
+++ b/packages/opencode/src/plugin/mimo.ts
@@ -99,7 +99,7 @@ export async function MimoAuthPlugin(_input: PluginInput): Promise {
},
methods: [
{
- label: "浏览器登录",
+ label: "Browser Login",
type: "oauth" as const,
authorize: async () => {
const { publicKey, privateKeyDer } = generateKeyPair()
@@ -159,7 +159,7 @@ export async function MimoAuthPlugin(_input: PluginInput): Promise {
return {
url: manualUrl,
method: "auto" as const,
- instructions: "在浏览器中完成授权,或粘贴 Code 完成登录。",
+ instructions: "Complete authorization in the browser, or paste the Code to finish login.",
callback: async (code?: string) => {
if (code) {
try {
diff --git a/packages/opencode/src/tool/actor.ts b/packages/opencode/src/tool/actor.ts
index 0c773d82..55f29f97 100644
--- a/packages/opencode/src/tool/actor.ts
+++ b/packages/opencode/src/tool/actor.ts
@@ -452,13 +452,27 @@ export const ActorTool = Tool.define(
// root-level union and passes through unchanged — root keeps exactly one
// key (`operation`), so models can't drop the discriminator.
operation: z
- .discriminatedUnion("action", [
- runSchema,
- spawnSchema,
- statusSchema,
- waitSchema,
- cancelSchema,
- sendSchema,
+ .union([
+ // Handle models that stringify the operation object (#561)
+ z.string().transform((val, ctx) => {
+ try {
+ const parsed = JSON.parse(val)
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed
+ } catch {}
+ ctx.issues.push({
+ code: z.ZodIssueCode.custom,
+ message: `operation must be a JSON object, got string: "${val.substring(0, 100)}${val.length > 100 ? "..." : ""}"`,
+ })
+ return z.NEVER
+ }),
+ z.discriminatedUnion("action", [
+ runSchema,
+ spawnSchema,
+ statusSchema,
+ waitSchema,
+ cancelSchema,
+ sendSchema,
+ ]),
])
.meta({ type: "object" }),
})
diff --git a/packages/opencode/src/util/process.ts b/packages/opencode/src/util/process.ts
index 96c35e5d..8a5cfb21 100644
--- a/packages/opencode/src/util/process.ts
+++ b/packages/opencode/src/util/process.ts
@@ -65,6 +65,7 @@ export function spawn(cmd: string[], opts: Options = {}): Child {
env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined,
stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"],
windowsHide: process.platform === "win32",
+ detached: process.platform !== "win32", // Detach on Unix to prevent terminal takeover
})
let closed = false
diff --git a/packages/opencode/test/plugin/mimo.test.ts b/packages/opencode/test/plugin/mimo.test.ts
index 7f8ba73c..adbc19f7 100644
--- a/packages/opencode/test/plugin/mimo.test.ts
+++ b/packages/opencode/test/plugin/mimo.test.ts
@@ -78,7 +78,7 @@ describe("MimoAuthPlugin", () => {
test("has one login method", async () => {
const hooks = await MimoAuthPlugin(fakeInput)
expect(hooks.auth!.methods).toHaveLength(1)
- expect(hooks.auth!.methods[0].label).toBe("浏览器登录")
+ expect(hooks.auth!.methods[0].label).toBe("Browser Login")
expect(hooks.auth!.methods[0].type).toBe("oauth")
})
})
diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx
index 2d4e2bdd..aadca6bc 100644
--- a/packages/ui/src/hooks/use-filtered-list.tsx
+++ b/packages/ui/src/hooks/use-filtered-list.tsx
@@ -36,9 +36,9 @@ export function useFilteredList(props: FilteredListProps) {
(x) => {
if (!needle) return x
if (!props.filterKeys && Array.isArray(x) && x.every((e) => typeof e === "string")) {
- return fuzzysort.go(needle, x).map((x) => x.target) as T[]
+ return fuzzysort.go(needle, x, { threshold: -10000 }).map((x) => x.target) as T[]
}
- return fuzzysort.go(needle, x, { keys: props.filterKeys! }).map((x) => x.obj)
+ return fuzzysort.go(needle, x, { keys: props.filterKeys!, threshold: -10000 }).map((x) => x.obj)
},
groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
entries(),