Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c755968
feat(ui): make configuration pickers compact and inline
kevinjosethomas Sep 10, 2026
32e865c
test(ui): update inline onboarding regression coverage
kevinjosethomas Sep 10, 2026
cf53afe
fix(tui): split configuration pickers and drop catalog rates label
kevinjosethomas Sep 10, 2026
fe834d5
feat(tui): right-align model providers and prioritize signed-in ordering
kevinjosethomas Sep 10, 2026
162659e
feat(tui): add per-model effort squares to the models picker
kevinjosethomas Sep 10, 2026
7e64a0a
fix(tui): keep signed-in models first in picker search results
kevinjosethomas Sep 10, 2026
bdb6cc8
fix(tui): soften the picker selection highlight
kevinjosethomas Sep 10, 2026
ef0c2cf
fix(tui): align and label the picker effort squares
kevinjosethomas Sep 10, 2026
44f506d
fix(tui): fold the pricing unit into the model detail header
kevinjosethomas Sep 10, 2026
fda835a
fix(tui): round model picker token prices
kevinjosethomas Sep 10, 2026
17a3326
fix(tui): center and restyle the picker effort squares
kevinjosethomas Sep 10, 2026
1c55641
fix(tui): drop the models picker header text
kevinjosethomas Sep 10, 2026
550d97f
fix(tui): use square glyphs for the effort marks
kevinjosethomas Sep 10, 2026
cb7265e
fix(tui): move the pricing unit onto the price row
kevinjosethomas Sep 10, 2026
4c6c64e
fix(tui): fix the effort cluster width across level changes
kevinjosethomas Sep 10, 2026
d0d8bf5
fix(tui): refine effort squares and the pricing unit
kevinjosethomas Sep 10, 2026
eb0f79c
fix(tui): drop the model detail header line
kevinjosethomas Sep 10, 2026
2d27204
fix(tui): use the largest square glyphs for effort marks
kevinjosethomas Sep 10, 2026
d07c77f
fix(tui): tighten the effort square spacing
kevinjosethomas Sep 10, 2026
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
18 changes: 18 additions & 0 deletions packages/coding-agent/.changes/ui-inline-pickers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
- Changed model, provider, and MCP pickers to compact inline lists with responsive search and keyboard navigation.
- Added selected-model catalog prices for input, cached input, and output per million tokens.
- Split the configuration menu into separate single-purpose pickers and dropped the tab bar and tab navigation; each command opens only its own picker.
- Changed model rows to right-align the provider label with a require sign in hint beside it, and to list signed-in providers first with Prime Inference pinned on top when signed in.
- Added per-model effort squares to the models picker; left/right adjusts the highlighted model's reasoning level and Enter applies the model and effort together.
- Fixed the models picker search to keep signed-in providers above unsigned matches, with Prime Inference pinned on top of the signed-in group.
- Softened the selected row highlight in menu pickers: the selection background blends toward the editor surface and the selected label renders bold instead of accent-colored.
- Refined the effort squares: clusters align across rows with arrow hints on the highlighted row, spaced squares in a stronger purple, and the selected level labeled beside them.
- Folded the USD per million tokens unit into the model detail header line and left clear whitespace at the end of the detail block.
- Rounded model picker token prices to at most three decimals, showing sub-$0.001 rates as <0.001 instead of a misleading $0.
- Centered the models picker effort cluster near the row midpoint with square glyphs; fills render light gray and reserve the saturated purple for the highlighted row.
- Removed the explanatory title and subtitle lines from the model, provider, and MCP pickers; the search row now leads each picker.
- Moved the model detail pricing unit onto the price row as "dollars per 1 million tokens" and left the provider/model line bare.
- Fixed the effort cluster so changing the level never shifts the row; the level label renders in a fixed-width cell sized to the longest supported level name.
- Refined the model picker effort marks to the larger medium-square glyphs, softened the effort purple, and shortened the pricing unit to "$ / 1M tokens".
- Dropped the provider/model-id line from the inline model detail block; the prices now follow the list row directly.
- Settled the effort marks on the filled ■ and empty □ squares, the largest square pair the terminal fonts cover.
- Tightened the effort square spacing; the squares now render edge to edge and the cluster stays centered.
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import type { Api, Model } from "@earendil-works/pi-ai";
import {
type Component,
Container,
type Focusable,
getKeybindings,
type TUI,
visibleWidth,
wrapTextWithAnsi,
} from "@earendil-works/pi-tui";
import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
import { Container, type Focusable, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
import type { AuthStorage } from "../../../core/auth-storage.js";
import type { ModelRegistry } from "../../../core/model-registry.js";
import { theme } from "../theme/theme.js";
import { keyText } from "./keybinding-hints.js";
import { getMenuPanelInnerWidth } from "./menu-panel.js";
import { ModelSelectorComponent } from "./model-selector.js";
import { type AuthSelectorProvider, OAuthSelectorComponent } from "./oauth-selector.js";

Expand All @@ -37,71 +28,15 @@ export interface ConfigurationMenuOptions {
configuredProviders: ReadonlySet<string>;
recentModels?: ReadonlyArray<string>;
initialModelSearch?: string;
thinkingLevel?: ModelThinkingLevel;
getRows?: () => number;
requestRender: () => void;
onSelectProvider: (provider: AuthSelectorProvider) => void;
onSelectMcpConnection: (provider: AuthSelectorProvider) => void;
onSelectModel: (model: Model<Api>) => void;
onSelectModel: (model: Model<Api>, thinkingLevel?: ModelThinkingLevel) => void;
onCancel: () => void;
}

const TAB_LABELS: Record<ConfigurationMenuTab, string> = {
providers: "Providers",
models: "Models",
"mcp-connections": "MCP Connections",
};

class ConfigurationMenuTabBar implements Component {
constructor(private readonly getActiveTab: () => ConfigurationMenuTab) {}

render(width: number): string[] {
return this.getLines(width);
}

getRowCount(width: number): number {
return this.getLines(width).length;
}

private getLines(width: number): string[] {
const safeWidth = Math.max(1, width);
const activeTab = this.getActiveTab();
const labels = CONFIGURATION_MENU_TABS.map((tab) => {
const label = `[${tab === activeTab ? "▶" : " "} ${TAB_LABELS[tab]}]`;
return tab === activeTab ? theme.bold(theme.fg("accent", label)) : theme.fg("text", label);
});
const lines = this.wrapItems(
[theme.bold(theme.fg("muted", "Tabs:")), ...labels],
theme.fg("muted", " "),
safeWidth,
);
const tabKey = keyText("tui.input.tab", { primaryOnly: true });
const shiftTabKey = keyText("app.configuration.previousTab", { primaryOnly: true });
const closeKey = keyText("tui.select.cancel", { primaryOnly: true });
const hint = `${theme.fg("dim", `${tabKey}/${shiftTabKey}`)}${theme.fg("muted", " switch tabs · ")}${theme.fg("dim", closeKey)}${theme.fg("muted", " close")}`;
return [...lines, ...wrapTextWithAnsi(hint, safeWidth)];
}

private wrapItems(items: string[], separator: string, width: number): string[] {
const lines: string[] = [];
let line = "";
for (const item of items) {
const candidate = line ? `${line}${separator}${item}` : item;
if (line && visibleWidth(candidate) > width) {
lines.push(...wrapTextWithAnsi(line, width));
line = item;
} else {
line = candidate;
}
}
if (line) {
lines.push(...wrapTextWithAnsi(line, width));
}
return lines;
}

invalidate(): void {}
}

export class ConfigurationMenuComponent extends Container implements Focusable {
private readonly bodies: {
providers: OAuthSelectorComponent;
Expand All @@ -110,13 +45,11 @@ export class ConfigurationMenuComponent extends Container implements Focusable {
};
private activeTab: ConfigurationMenuTab;
private _focused = false;
private renderWidth = 78;

constructor(private readonly options: ConfigurationMenuOptions) {
super();
this.activeTab = options.initialTab;
const tabBar = new ConfigurationMenuTabBar(() => this.activeTab);
const getHeaderRows = () => tabBar.getRowCount(getMenuPanelInnerWidth(this.renderWidth)) + 1;
const getRows = () => Math.max(1, (options.getRows?.() ?? 24) - 1);
const providerOptions = options.providerOptions.filter(
(provider) => (provider.category ?? "provider") === "provider",
);
Expand All @@ -130,11 +63,10 @@ export class ConfigurationMenuComponent extends Container implements Focusable {
options.onCancel,
(providerId) => options.modelRegistry.getProviderAuthStatus(providerId),
{
getRows: options.getRows,
header: tabBar,
getHeaderRows,
title: "Providers",
subtitle: "Connect with a subscription or API key.",
getRows,
inline: true,
title: "",
subtitle: "",
searchPlaceholder: "Search providers",
},
);
Expand All @@ -149,10 +81,10 @@ export class ConfigurationMenuComponent extends Container implements Focusable {
{
availableModels: options.availableModels,
configuredProviders: options.configuredProviders,
header: tabBar,
getHeaderRows,
getRows: options.getRows,
getRows,
inline: true,
recentModels: options.recentModels,
thinkingLevel: options.thinkingLevel,
},
);
const mcpConnections = new OAuthSelectorComponent(
Expand All @@ -163,12 +95,12 @@ export class ConfigurationMenuComponent extends Container implements Focusable {
options.onCancel,
(providerId) => options.modelRegistry.getProviderAuthStatus(providerId),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dead configuration tab keybinding

Low Severity

Removing tab navigation left app.configuration.previousTab with no non-test consumer. The exported keybinding still ships in the public map, so custom keybindings.json entries and Shift+Tab resolve to a no-op after the completed picker split.

Fix in Cursor Fix in Web

Triggered by project rule: Review rules

Reviewed by Cursor Bugbot for commit cf53afe. Configure here.

{
getRows: options.getRows,
header: tabBar,
getHeaderRows,
title: "MCP Connections",
subtitle: "Connect MCP integrations and service credentials.",
getRows,
inline: true,
title: "",
subtitle: "",
searchPlaceholder: "Search MCP connections",
emptyMessage: "No MCP connections. Use /mcp add to configure a server.",
},
);

Expand All @@ -190,8 +122,17 @@ export class ConfigurationMenuComponent extends Container implements Focusable {
}

override render(width: number): string[] {
this.renderWidth = width;
return super.render(width);
const selectKey = keyText("tui.select.confirm", { primaryOnly: true });
const closeKey = keyText("tui.select.cancel", { primaryOnly: true });
const navigate = `${keyText("tui.select.up", { primaryOnly: true })}/${keyText("tui.select.down", { primaryOnly: true })}`;
const effort = `${keyText("tui.editor.cursorLeft", { primaryOnly: true })}/${keyText("tui.editor.cursorRight", { primaryOnly: true })}`;
const hint =
width >= 70
? this.activeTab === "models"
? `${navigate} model · ${effort} effort · ${selectKey} select · ${closeKey} close`
: `${navigate} navigate · ${selectKey} select · ${closeKey} close`
: `${selectKey} select · ${closeKey} close`;
return [...super.render(width), truncateToWidth(theme.fg("dim", ` ${hint}`), width, "", true)];
}

getActiveTab(): ConfigurationMenuTab {
Expand Down Expand Up @@ -227,32 +168,10 @@ export class ConfigurationMenuComponent extends Container implements Focusable {
}

handleInput(keyData: string): void {
const kb = getKeybindings();
if (kb.matches(keyData, "tui.input.tab")) {
this.switchTab(1);
return;
}
if (kb.matches(keyData, "app.configuration.previousTab")) {
this.switchTab(-1);
return;
}
if (
this.activeTab === "models" &&
(kb.matches(keyData, "tui.editor.cursorLeft") || kb.matches(keyData, "tui.editor.cursorRight"))
) {
this.activeBody.getSearchInput().handleInput(keyData);
return;
}
this.activeBody.handleInput(keyData);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium components/configuration-menu.ts:171

Left/right input is consumed by ModelSelectorComponent for non-reasoning models, so users cannot move the search cursor or close the picker with left-at-start. Restore the model-tab handling that forwards these keys directly to MenuSearchInput before delegating to activeBody.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/interactive/components/configuration-menu.ts around line 171:

Left/right input is consumed by `ModelSelectorComponent` for non-reasoning models, so users cannot move the search cursor or close the picker with left-at-start. Restore the model-tab handling that forwards these keys directly to `MenuSearchInput` before delegating to `activeBody`.

Evidence trail:
Reviewed commit 162659e6ffedae38d6ac309dd6bb825b7784b8cb
- packages/coding-agent/src/modes/interactive/components/configuration-menu.ts:170-172
- packages/coding-agent/src/modes/interactive/components/model-selector.ts:379-400, 583-627
- packages/coding-agent/src/modes/interactive/components/modal-back.ts:26-30
- packages/coding-agent/src/core/keybindings.ts:157

}

private get activeBody(): OAuthSelectorComponent | ModelSelectorComponent {
return this.bodies[this.activeTab];
}

private switchTab(direction: 1 | -1): void {
const currentIndex = CONFIGURATION_MENU_TABS.indexOf(this.activeTab);
const nextIndex = (currentIndex + direction + CONFIGURATION_MENU_TABS.length) % CONFIGURATION_MENU_TABS.length;
this.setActiveTab(CONFIGURATION_MENU_TABS[nextIndex] ?? "providers");
}
}
Loading
Loading