Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/app/src/components/prompt-input/editor-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ function MimoOAuthFlow(props: { url: string; instructions: string }) {
<text fg={theme.primary}>{props.url}</text>
</Show>
<Show when={props.instructions}>
<text fg={theme.textMuted}>{props.instructions}</text>
<text fg={theme.textMuted}>{t("tui.dialog.login.flow.instructions")}</text>
</Show>
<text fg={theme.textMuted}>{t("tui.dialog.login.flow.waiting")}</text>
</box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ export function DialogModel(props: { providerID?: string }) {
]}
onFilter={setQuery}
flat={true}
skipFilter={true}
title={title()}
current={local.model.current()}
/>
Expand Down
9 changes: 7 additions & 2 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 (
<Show when={busyMessage()}>
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/tui/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ export const dict: Record<string, string> = {
"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?",
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/tui/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 智能体开始实现?",
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/plugin/mimo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export async function MimoAuthPlugin(_input: PluginInput): Promise<Hooks> {
},
methods: [
{
label: "浏览器登录",
label: "Browser Login",
type: "oauth" as const,
authorize: async () => {
const { publicKey, privateKeyDer } = generateKeyPair()
Expand Down Expand Up @@ -159,7 +159,7 @@ export async function MimoAuthPlugin(_input: PluginInput): Promise<Hooks> {
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 {
Expand Down
28 changes: 21 additions & 7 deletions packages/opencode/src/tool/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }),
})
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/util/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/test/plugin/mimo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/hooks/use-filtered-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
(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(),
Expand Down