Skip to content

tui: model favorites toggle missing hint and dead in /models dialog (2.0.13) #50969

Closed

Description

@cannockwolf

Description

Summary

Model favourites can't be toggled in the V2 terminal: the /models dialog shows no favourite hint, and neither ctrl+f nor remapped keys star a model. Backend favourites still exist, so this looks like a V1→V2 compatibility regression.

Environment

  • opencode version: 2.0.13
  • OS: Linux 6.18.39-haos (x86_64, Home Assistant add-on container; client viewed from macOS Chrome)
  • Terminal: Chrome on macOS → Home Assistant OpenCode add-on terminal (Ingress ttyd/xterm). Cmd+F is browser Find.
  • Shell: /bin/bash
  • Install/channel: latest, certified add-on build (autoupdate disabled; ships with add-on 3.0.5)
  • Active plugins: TUI uses managed config with only file:///opt/opencode-v2-homeassistant/runtime-guard-plugin; no user plugins

Reproduction

  1. Open the OpenCode terminal in Chrome on macOS.
  2. Open the model list via <leader>m (or /models).
  3. Press Esc, highlight a model row with up/down (filter input not focused).
  4. Press ctrl+f — nothing happens.
  5. Remap in cli.json: {"keybinds": {"model.dialog.favorite": ["ctrl+f", "alt+f"]}}, then ["ctrl+f", "f4"] — reload, repeat step 4 with Alt+F / F4 — still nothing.

Expected Behavior

Per V2 keybind docs, model.dialog.favorite defaults to ctrl+f and toggles favourite; V1 showed a "ctrl-f to save favourite" hint. Expect the hint and a working toggle (or remappable equivalent).

Actual Behavior

No favourite hint anywhere in the dialog. No key (Ctrl+F, Alt+F, F4) toggles favourite. Consistent since updating. TUI log shows clean start with no keybind rejection; model.json state still holds 3 existing favourites.

Additional Context

  • Today's update: add-on 2.5.6 → 3.0.3 → 3.0.5 (3.0.0 = "OpenCode V2 has arrived"), opencode 2.0.13.
  • V1 key was model_favorite_toggle ctrl+f; V2 docs list model.dialog.favorite ctrl+f.
  • ctrl+f is overloaded in V2 (session.pin.toggle, permission.prompt.fullscreen, input.move.right), and Chrome reserves Cmd+F / Ctrl+F for Find, so browser delivery is part of it — but the missing hint and dead F4 remap point beyond key interception.
  • Temporary cli.json remap tested at /run/opencode-v2/tui/config/opencode/cli.json (ephemeral, resets on reboot); valid JSON, no effect.

Plugins

No response

OpenCode version

No response

Steps to reproduce

No response

Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Activity

  1. cannockwolf commented on Sep 23, 2026

    @cannockwolf
    Author

    favourites + hint returned after /connect as i got disconneted from openai and had to reconnect

  2. argszero commented on Sep 26, 2026

    @argszero
    Contributor

    I looked into this on v2 and can reproduce the cause: this is a regression from the redefinition of connected().

    On dev, useConnected() reports whether a provider is configured (packages/tui/src/component/use-connected.tsx). On v2 it delegates to hasConnectedProvider, i.e. "some integration has connections" (packages/tui/src/util/connected-provider.ts: integrations.some((integration) => integration.connections.length > 0)).

    The model dialog carried the old gates over verbatim, so with no integration connected:

    • packages/tui/src/component/dialog-model.tsx:160 hides the model.dialog.favorite action (hidden: !connected())
    • dialog-model.tsx:32 drops the favorites list, and :27/:31 gate the Favorites and Recents sections through showExtra
    • dialog-model.tsx:102 also feeds connected() into the option set

    The missing hint and the dead toggle are the same cause: packages/tui/src/ui/dialog-select.tsx builds its keymap from shownActions() (:156 filters !item.hidden, :162 registers from it), so a hidden action is not just invisible, its keybinding is never registered and ctrl+f does nothing at all.

    Your follow-up comment matches exactly: favorites and their hint returned as soon as an integration was connected again. Local favorites and recents are client-side preference state, so gating them on integration connectivity looks unintended; only the integration-facing entry (model.dialog.provider, title at :147) should depend on it.

    I would like to work on this. Plan: stop gating the favorites list, the Favorites/Recents sections and the model.dialog.favorite action on integration connectivity, keep the gate for the integration entry, and add a dialog test covering the no-integration case.

  3. argszero commented on Sep 28, 2026

    @argszero
    Contributor

    Opened #51760 (v2) with the fix.

    Favorites and recents are client side preference state, so they are no longer gated on integration connectivity: the Favorites and Recent sections are listed again, the favorites list is read unconditionally, and the model.dialog.favorite action is always shown. Showing the action is what brings back both the hint and the binding, because DialogSelect registers its keymap entries from shownActions() and a hidden action is never bound. The integration entry keeps its connected() gate, since it is the only integration facing part of this dialog.

    Two tests mount the real dialog on a fixture with no connected integration: the footer renders the ctrl+f hint and pressing it saves the highlighted model, and favorites already stored in model.json are listed under Favorites. Both fail before the change, where the captured frame shows only View all integrations ctrl+a.

  4. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed report. I can reproduce this on 2.0.13, and it still happens on the latest dev (fa1260d).

    Steps: with a model provider available but no connected integration, open /models and highlight a model. The dialog shows no favourite hint, only "View all integrations ctrl+a". ctrl+f, and f4 after remapping model.dialog.favorite in cli.json, do nothing. The recording below shows the whole sequence, and the script below reproduces it.

    This is not your terminal or browser: the favourite action is only shown when an integration has a saved connection. While it is hidden, no key is bound to it at all, which is why remapping made no difference. That also matches what you saw: your add-on probably provides models without a saved connection. A maintainer will look at it. Related: #51760 (a closed PR describing the same cause) and #49309 (same "connected" check, affecting provider names in /models).

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#50969: with no connected integration, the /models
    // dialog shows no favorite hint and ctrl+f (or a remapped key) does not toggle favorites.
    import { Effect } from "effect"
    import { OpenCodeDriver } from "opencode-drive"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        // Also remap the action to f4 to show that remaps have no effect either.
        tuiConfig: { keybinds: { "model.dialog.favorite": ["ctrl+f", "f4"] } },
        tui: { recording: true, keypressOverlay: true },
      },
      ({ ui, opencode }) =>
        Effect.gen(function* () {
          yield* ui.waitFor((s: any) => true)
          yield* Effect.sleep(1500)
          yield* ui.submit("/models")
          yield* Effect.sleep(1500)
          yield* ui.screenshot("models-open")
          yield* ui.arrow("down")
          yield* ui.arrow("down")
          yield* Effect.sleep(300)
          yield* ui.screenshot("highlighted")
          yield* ui.press("f", { ctrl: true })
          yield* Effect.sleep(800)
          yield* ui.screenshot("after-ctrl-f")
          yield* ui.press("f4")
          yield* Effect.sleep(800)
          yield* ui.screenshot("after-f4")
          const frame = yield* ui.capture()
          yield* Effect.log("FRAME:\n" + (frame as any).lines.map((l: any) => l.spans.map((s: any) => s.text).join("")).join("\n"))
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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