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(),