Skip to content

fix: custom provider model list overflow and add /provider command#3766

Merged
esengine merged 3 commits into
esengine:main-v2from
lanshi17:fix/provider-model-list-scroll-and-provider-command
Jun 11, 2026
Merged

fix: custom provider model list overflow and add /provider command#3766
esengine merged 3 commits into
esengine:main-v2from
lanshi17:fix/provider-model-list-scroll-and-provider-command

Conversation

@lanshi17

Copy link
Copy Markdown
Contributor

Summary

Fixes #3765

Two improvements to the CLI TUI:

Bug Fix: Model list overflow in terminal selection menus

When a custom provider returns a large model list (100+ models), selectOne/selectMany menus rendered all items without viewport windowing — items beyond the terminal height were invisible and unreachable.

Changes to internal/cli/select.go:

  • Added terminal height detection via term.GetSize(fd)
  • Implemented viewport windowing: only shows items that fit in the terminal
  • Added scroll indicators: "↑ N more above" / "↓ N more below"
  • Added / key to enter keyword search mode (case-insensitive filter on name + desc)
  • Esc exits search and restores the full list
  • Works for both selectOne (single-choice) and selectMany (multi-choice) menus

New Feature: /provider slash command

Added a /provider command to switch providers from the CLI chat interface:

  • /provider — lists all configured providers with model counts, marks active
  • /provider <name> — switches to that provider's default model (or lists models when multiple are configured)
  • Tab autocomplete for provider names
  • Bilingual i18n support (en/zh)

Files Changed

File Change
internal/cli/select.go Viewport scrolling + search mode
internal/cli/provider.go New /provider command (new file)
internal/cli/chat_tui.go Register /provider in slash handler
internal/cli/complete.go /provider autocomplete + provider data
internal/cli/help_view.go /provider in help listing
internal/cli/model.go providerNames() helper
internal/control/slash.go /provider arg completion + ProviderNames in ArgData
internal/control/slash_test.go Test coverage for /provider completion
internal/i18n/i18n.go New message fields
internal/i18n/messages_en.go English translations
internal/i18n/messages_zh.go Chinese translations

Testing

  • go build ./... — passes
  • go vet ./... — passes
  • go test ./internal/i18n/... — passes (drift guard confirms en/zh parity)
  • go test ./internal/control/... — passes (includes new /provider test cases)
  • go test ./internal/config/... — passes

… command

- Fix selectOne/selectMany overflow: when the model list exceeds the
  terminal height, the menu now shows a viewport-sized window with scroll
  indicators (↑ N more above / ↓ N more below) instead of rendering all
  items off-screen.
- Add keyword search: pressing '/' in any selectOne/selectMany menu
  enters a search mode that filters items by name/desc substring match
  (case-insensitive). Esc exits search and restores the full list.
- Add /provider slash command: lists configured providers and their
  models; '/provider <name>' switches to that provider's default model
  (or lists models for selection when multiple are configured).
- Add provider name autocomplete for /provider <Tab>.
- Add /provider to /help listing.
- Add i18n messages for all new UI strings (en + zh).
- Add test coverage for /provider arg completion in control/slash_test.go.

Closes esengine#3765
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development tui Terminal UI / CLI (internal/cli, internal/control) agent Core agent loop (internal/agent, internal/control) labels Jun 10, 2026

@esengine esengine left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lanshi17 — this is a lot of solid work aimed squarely at the right problem.

The /provider command is nicely integrated: completion, help, i18n en/zh parity, and a test for the arg completion. Deferring multi-model selection to /model instead of launching a raw-mode picker inside the bubbletea loop is exactly the right call, and the comment explains why. The viewport scrolling also does the main job from #3765 — with plain arrow/jk navigation the window tracks the selection, so models past the terminal height are reachable again.

One thing I'd like fixed before it goes in, and it's specifically in the search/filter redraw, not the scrolling:

On each filter keystroke the code reassigns filtered and then moves the cursor up by totalLines() computed from the new (post-filter) list:

filtered = filterMenuItems(items, searchQuery)   // e.g. 100 -> 3
...
fmt.Fprintf(w, "\033[%dA", totalLines())          // moves up by the NEW height
drawHeader(); render()

The previous frame was drawn at the old height. When a filter narrows the list below the viewport — the headline "100+ models, type to filter" path — the move-up is shorter than what was printed last frame, and the extra rows are never cleared: render() only clears each line it rewrites (\033[K), nothing below it. So typing a filter leaves stale rows on screen and drifts the cursor. (There's also a 1-line off-by-one when first entering search, since totalLines() counts the search bar before it's drawn.)

The robust fix is to make the redraw independent of how the height changes between frames: track the number of lines actually printed last frame and move up by that, then emit \033[J (clear to end of screen) right after the redraw so any rows below are wiped. That covers both the narrowing case and the enter-search case.

Two small things while you're in there (non-blocking):

  • ProviderSwitchingFmt / ProviderSwitchedFmt are added to i18n but never used — drop them or wire them in.
  • select.go has no automated coverage (understandable for raw-mode TTY), which is why this slipped past CI. A tiny unit test on the line-accounting (the printed-line count for a given filtered size) would guard the regression.

Really nice contribution — once the search redraw clears properly this is good to merge. Thanks again!

@SivanCola

Copy link
Copy Markdown
Collaborator

作者你好,感谢这个 PR。这个方向没问题,但目前还有几处需要改一下:

  1. /provider 列表在 provider 只有非 chat 模型时会 panic。showProviders()ChatModelList() 为空后虽然用 ModelList() 算了数量,但单模型展示仍然取 models[0],会越界。请统一使用 fallback 后的模型列表。

  2. selectOne/selectMany 的 viewport 行数计算仍然会超过终端高度。当前 maxViewport() 只减了 3 行,但实际渲染包含 header+blank、上下滚动提示,搜索模式还多一行 search bar。需要把固定行数按实际渲染扣掉,否则 24 行终端仍会渲染 25/26 行。

  3. 搜索过滤后重绘会留下旧菜单残影。因为更新 filtered 后才调用 totalLines(),从多行过滤到少量/零结果时只上移新高度,没有清掉旧高度。建议保存上一次渲染行数,用旧行数回退并清屏/重绘。

  4. /provider 目前只接入了 chat TUI。shared completion 已经加了 /provider,但 desktop 的 ArgData 没填 ProviderNames/CurrentProviderCommands() 也没列出 /provider,controller 的 managementNotice 也没有处理,所以 desktop/HTTP 路径会出现空补全或 unknown command。请补齐这些路径,或者先不要把它作为 shared slash command 暴露。

我这边验证过:隔离 HOME 后 go test ./internal/cli ./internal/control ./internal/i18n 通过,cd desktop && go test ./... 也通过。当前主要是上述行为和边界问题需要修。

…ration

1. Search redraw stale rows: track prevLines (lines actually printed in
   the last frame), move cursor up by prevLines (not the new height),
   and emit \033[J (clear-to-end-of-screen) after each redraw to wipe
   stale rows left by a taller previous frame.

2. Panic in showProviders(): use the unified fallback model list
   (ChatModelList → ModelList) for both the count and the single-model
   display label. Previously models[0] could panic on an empty slice
   when only non-chat models were configured.

3. Viewport height: fixedLines() now returns the exact non-item line
   count (4 normal, 5 with search bar) instead of a hardcoded 3.
   maxViewport() accepts a searching flag and subtracts the right
   overhead. Frame rows are padded to a constant height so cursor
   positioning stays stable.

4. Remove unused i18n fields: ProviderSwitchingFmt, ProviderSwitchedFmt.

5. Desktop integration:
   - Commands(): add /provider to the built-in command list.
   - SlashArgs(): populate ArgData.ProviderNames and CurrentProvider
     from the Models() result.
   - managementNotice(): handle /provider (list) and /provider <name>
     (switch) for the desktop/HTTP Submit path.
   - Add providerListText() and providerSwitchText() controller methods.

6. Tests: add select_test.go with FrameLines, MaxViewportBounds, and
   FilterMenuItems unit tests. Add provider name completion test in
   control/slash_test.go.
@github-actions github-actions Bot added the desktop Wails desktop app (desktop/**) label Jun 10, 2026
@lanshi17 lanshi17 requested a review from esengine June 10, 2026 04:08
@lanshi17

Copy link
Copy Markdown
Contributor Author

All review items addressed in the latest push. Here's a summary of what changed:

Search redraw stale rows (both reviewers)

  • Replaced totalLines() with prevLines tracking — the redraw now moves the cursor up by the number of lines actually printed in the previous frame, not the new frame's height.
  • Added \033[J (clear-to-end-of-screen) after each redraw to wipe stale rows left by a taller previous frame.
  • The off-by-one when entering search is also fixed since prevLines is computed after the draw, not before.

Panic in showProviders() (@LanShi's #1)

  • Unified the model list: ChatModelList() → fallback to ModelList(). The same models slice is used for both the count and the single-model display label, so models[0] can't panic on an empty slice.

Viewport height (@LanShi's #2)

  • fixedLines(searching) now returns the exact non-item line count: 4 (header + blank + scroll-up + scroll-down) or 5 with search bar.
  • maxViewport() accepts a searching flag and subtracts the correct overhead.
  • Menu rows are padded to a constant viewport height so cursor positioning stays stable across frames.

Unused i18n fields

  • Removed ProviderSwitchingFmt and ProviderSwitchedFmt from the struct and both catalogues.

Desktop/HTTP integration (@LanShi's #4)

  • desktop/app.go Commands(): added /provider to the built-in command list.
  • desktop/app.go SlashArgs(): populated ArgData.ProviderNames and CurrentProvider from the Models() result.
  • internal/control/slash.go managementNotice(): added /provider case — lists providers (no arg) or shows models for a named provider.
  • Added providerListText() and providerSwitchText() controller methods.

Tests

  • New internal/cli/select_test.go: TestFrameLines (7 cases), TestFrameLinesNeverExceedsTerminal (exhaustive 0–200 items × searching), TestMaxViewportBounds, TestFilterMenuItems.
  • Lint fix: b.WriteString(fmt.Sprintf(...))fmt.Fprintf(&b, ...).

CI: 14/14 passing

@esengine esengine left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the latest push end to end: the redraw now tracks prevLines (lines actually printed last frame) and emits \033[J after each frame, which covers both the narrowing-filter case and the enter-search off-by-one; showProviders/switchToProvider share one fallback model slice so the empty-chat-list panic is gone; fixedLines/maxViewport account for the real per-frame overhead with the searching flag; and the desktop/HTTP paths now ship /provider completion + managementNotice handling instead of dangling shared-completion entries. The exhaustive TestFrameLinesNeverExceedsTerminal sweep is exactly the regression guard I asked for.

Thanks for the quick, thorough turnaround — merging.

@esengine esengine merged commit 53eb72c into esengine:main-v2 Jun 11, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) desktop Wails desktop app (desktop/**) tui Terminal UI / CLI (internal/cli, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: custom provider model list overflow and add /provider command

3 participants