Skip to content

Model picker tooltip glues modality names together and cuts off #61594

Description

@XKCPU4444

Description

When I hover a model in the model picker, the tooltip in the corner shows the

Image

input modalities as one unreadable word, e.g. "TextImagePDFAudioVideoTo…".
I can't tell which models actually accept images.

Plugins

No response

OpenCode version

opencode-cli --version(2.2.5)

Steps to reproduce

  1. Open the model picker.
  2. Hover any model row.
  3. Look at the tooltip on the right edge — the bottom line is the modality list.

Screenshot

No response

Operating System

Windows 11

Terminal

No response

Activity

  1. opencodejr commented on Nov 4, 2026

    @opencodejr
    Contributor

    Thanks for the report. I could reproduce this on the latest dev (ffe6fed). The model picker code behind this tooltip hasn't changed since 2.2.5.

    I rendered the model details tooltip from the model picker for a model that accepts text, image, PDF, audio and video and supports tools:

    • With the app language set to English, the bottom line is Text, Image, PDF, Audio, Video, Tools.
    • With it set to 简体中文, the same line is TextImagePDFAudioVideoTools, which matches your screenshot. The trailing "To…" is "Tools" being cut off.

    The list is built with the browser's locale-aware list formatter. In the "unit" style it uses, Simplified Chinese puts nothing between items. These labels also don't have Chinese translations yet, so the English words come out run together. Until it's fixed, switching the app language to English (or 繁體中文, which uses spaces) should make the list readable.

    The script below reproduces it as a component test.

    Reproduction script (repro-61594.test.ts)
    // Repro for anomalyco/opencode#61594: the compact model details tooltip in the model picker glues
    // modality names together ("TextImagePDFAudioVideoTools") when the app language is Simplified Chinese.
    // Copy into packages/app/test-browser/ of an opencode checkout (after `bun install`) and run from packages/app:
    //   bun test --conditions=browser --isolate --preload ../ui/test/solid-jsx.ts --preload ./happydom.ts ./test-browser/repro-61594.test.ts
    import { expect, test } from "bun:test"
    import { createRequire } from "node:module"
    import { render } from "solid-js/web"
    import { dict as en } from "@/runtime/i18n/en"
    import { dict as zh } from "@/runtime/i18n/zh"
    import { DESKTOP_NATIVE_LOCALE_TAGS } from "@/runtime/i18n/desktop-native"
    
    // Same dictionary merge (English base + locale overrides) and Intl tag as runtime/i18n/language.tsx.
    let current: "en" | "zh" = "en"
    const dicts = { en, zh: { ...en, ...zh } } as Record<string, Record<string, string>>
    ;(globalThis as any).__repro61594Language = {
      intl: () => DESKTOP_NATIVE_LOCALE_TAGS[current],
      t: (key: string, params: Record<string, unknown> = {}) =>
        (dicts[current][key] ?? key).replace(/\{\{(\w+)\}\}/g, (_: string, name: string) => String(params[name] ?? "")),
    }
    
    // Bun does not compile Solid JSX: compile details.tsx with the same Babel preset as Vite.
    const require = createRequire(import.meta.url)
    const solid = createRequire(require.resolve("vite-plugin-solid"))
    const { transformSync } = solid("@babel/core")
    Bun.plugin.clearAll()
    Bun.plugin({
      name: "details-solid",
      setup(build) {
        build.onLoad({ filter: /[\\/]providers[\\/]models[\\/]details\.tsx$/ }, async (args) => ({
          contents: transformSync(
            (await Bun.file(args.path).text()).replace(
              'import { useLanguage } from "@/runtime/i18n/language"',
              "const useLanguage = () => globalThis.__repro61594Language",
            ),
            {
            filename: args.path,
            presets: [solid.resolve("babel-preset-solid"), solid.resolve("@babel/preset-typescript")],
            },
          ).code,
          loader: "js",
        }))
      },
    })
    
    const { ModelDetails } = await import("@/providers/models/details")
    
    const model: any = {
      id: "gemini-2.5-pro",
      name: "Gemini 2.5 Pro",
      provider: { id: "google", name: "Google" },
      limit: { context: 1_048_576 },
      capabilities: {
        reasoning: true,
        toolcall: true,
        input: { text: true, image: true, pdf: true, audio: true, video: true },
        output: { text: true, image: false, pdf: false, audio: false, video: false },
      },
    }
    
    for (const locale of ["en", "zh"] as const) {
      test(`compact model details summary (${locale})`, () => {
        current = locale
        const root = document.createElement("div")
        document.body.appendChild(root)
        const dispose = render(() => ModelDetails({ model, provider: "Google", compact: true, standalone: true }), root)
        const lines = [...root.querySelectorAll("div > div")].map((el) => el.textContent ?? "")
        console.log(`[${locale}] intl=${DESKTOP_NATIVE_LOCALE_TAGS[locale]}`, JSON.stringify(lines))
        const summary = lines.find((line) => line.includes("Text") || line.includes("文本"))
        expect(summary).toBeDefined()
        // Expected: the modalities are separated, e.g. "Text, Image, PDF, ..." — not run together.
        expect(summary).not.toContain("TextImage")
        dispose()
      })
    }

    Run it: bun repro-61594.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions