Skip to content

fix(tui): keys flow — typing can no longer be saved as an API key; explain unavailable-model redirect; fit keys rows; picker legend and order (#1403) - #1404

Merged
dennisonbertram merged 6 commits into
mainfrom
issue-keys-flow
Sep 6, 2026
Merged

fix(tui): keys flow — typing can no longer be saved as an API key; explain unavailable-model redirect; fit keys rows; picker legend and order (#1403)#1404
dennisonbertram merged 6 commits into
mainfrom
issue-keys-flow

Conversation

@dennisonbertram

@dennisonbertram dennisonbertram commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes #1403

Summary

A first-time user could corrupt their provider setup just by typing: selecting a model whose provider had no key silently opened the API Keys panel, letters typed while it was open leaked into the chat input, and the next Enter plus text was stored as an API key (reproduced: {"api_keys": {"deepseek": "/model"}}). This PR makes the panel explain why it opened, swallows typing while any panel is open (with a hint), rejects values that cannot be keys, fits the keys rows inside their box, names subscriptions correctly, adds a legend to the model picker, and sorts providers case-insensitively.

Scope and issue reconciliation

All five items in #1403 plus a sixth found while verifying (recorded on the issue): an OpenRouter-only model selected under the default Direct gateway was sent to OpenRouter with a rewritten id (deepseek-v4) and failed with 400. effectiveModelAndProvider now leaves OpenRouter-provider models untouched; the config panel shows "served only by OpenRouter" instead of a Direct choice. Nothing else. Out of scope as stated: per-provider key formats, keys panel redesign.

Impact analysis reconciliation

  • cmd/harnesscli/tui/model.go: overlay typing guard before the input fallthrough; key validation on confirm; apiKeyReason set on redirect and cleared on close/open; keys list view sizes its box and truncates rows; subscriptionLabel, truncateVisible, wrapPlain helpers.
  • components/modelswitcher: legendSuffix appended to the three footers when availability is known; provider order case-insensitive.
  • Overlays with their own text fields (model filter, dashboard input, search) handle runes in earlier arms; the full TUI suite (28 packages, race) is green, so none relied on the fallthrough.
  • Server, API, config: unchanged.

Architecture and duplication check

Searched rg 'Route to input area|activeOverlay = "apikeys"|ChatGPT subscription|sort.Strings(order)' cmd/harnesscli/tui. Changes sit in the existing handlers; no new component.

Test-first evidence

Red command: go test ./cmd/harnesscli/tui/... -run 'Overlay_Typed|APIKeys_|UnavailableSelection|FooterLegend|ReadyFirst|CaseInsensitive' (commit 35c5672)
Observed failure: typed text leaked into the chat input while the keys overlay was open: "hello there"; value "/model" must be rejected; status wrapped onto its own line: "│ not connected"; kimi-subscription must be labelled as a Kimi subscription; legend and order tests failing.
Green command: go test ./cmd/harnesscli/tui/... -race → 28 packages ok.

Verification evidence

Live tmux 120x40, real daemon with OpenRouter configured, built binary:

Picker with legend and case-insensitive order:

│                                                                                                                    │
│   Anthropic                                                                                                  (8) ○ │
│   cerebras                                                                                                   (1) ○ │
│   codex-subscription                                                                                         (1) ○ │
│   DeepSeek                                                                                                   (4) ● │
│   Google                                                                                                     (3) ○ │

Enter on an unavailable model now explains itself; rows fit; Kimi label:

                        │  Anthropic is not set up, so Claude Fable 5 cannot be used yet.      │
                        │  Enter its API key (ANTHROPIC_API_KEY) below: press Enter to         │
                        │  edit, Esc to go back.                                               │

Typing while the panel is open:

Press Esc to close this panel before typing a message

Confirming /model as a key:

That doesn't look like an API key (no spaces, doesn't start with /). Paste the key, or press Esc to cancel.

Routing fix, live: selecting deepseek/deepseek-v4-pro and sending a message now completes (captures in the PR comments).

Rollout and rollback

TUI-only; rebuild with scripts/install.sh, restart the TUI. Rollback: revert. Users who already stored a bogus key can overwrite it in /keys.

Documentation

website/docs/cli/tui.md note on the picker and keys panel; engineering-log entry.

Contract checklist

  • Linked issue follows the current structured contract and this PR closes it
  • Issue acceptance criteria, impact map, and scope were updated when the design changed
  • All callers, consumers, sources of truth, and similar abstractions were searched
  • No unrelated cleanup, hidden scope growth, duplicated wiring, or parallel abstraction was introduced
  • Tests were written first and the expected red failure was observed, or this is a strictly docs-only minor PR
  • Targeted checks and the repository-required full regression are green
  • Security, compatibility, lifecycle, deployment, observability, documentation, and rollback were reconciled
  • Real mouse/keyboard/API/operator behavior was exercised when the change is interaction- or integration-heavy

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5

dennisonbertram and others added 3 commits September 6, 2026 13:38
… key accepted, unexplained redirect, wrapped key rows, picker legend/order

Red output: TestOverlay_TypedRunesDoNotReachInput leaked "hello there"; TestAPIKeys_RejectsImplausibleKey accepted "/model"; TestAPIKeys_RowsFitBoxAndLabels: status wrapped onto its own line, kimi labelled ChatGPT; TestModelSwitcher_FooterLegend/ProviderOrderCaseInsensitive fail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
…en, reject implausible API keys, explain the unavailable-model redirect, fit keys rows, correct subscription labels, picker legend and case-insensitive provider order

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
…g-log entry

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

dennisonbertram and others added 3 commits September 6, 2026 13:47
…vider under the direct gateway; config panel explains routing

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
…ed under the direct gateway; config panel says so instead of offering Direct

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
…e panel; footer drops the gateway arrows for such models

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
@dennisonbertram
dennisonbertram merged commit ba576bd into main Sep 6, 2026
2 checks passed
@dennisonbertram
dennisonbertram deleted the issue-keys-flow branch September 6, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant