Skip to content

Commit c277dd8

Browse files
yethikrishnaclaude
andcommitted
fix: resolve TUI component crashes in provider commands
- Remove invalid `bold: true` style prop (not supported by @OpenTui) - Disable chat keyboard handler when provider modals are open - Simplify model picker, provider wizard, and settings panel components - Remove nested box layouts that could cause rendering issues Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 77d9806 commit c277dd8

4 files changed

Lines changed: 207 additions & 677 deletions

File tree

cli/src/chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1209,7 +1209,7 @@ export const Chat = ({
12091209
useChatKeyboard({
12101210
state: chatKeyboardState,
12111211
handlers: chatKeyboardHandlers,
1212-
disabled: askUserState !== null || reviewMode,
1212+
disabled: askUserState !== null || reviewMode || providerWizardMode || modelPickerMode || providerSettingsMode,
12131213
})
12141214

12151215
// Sync message block context to zustand store for child components

cli/src/components/model-picker.tsx

Lines changed: 52 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import React, { useState, useMemo, useCallback } from 'react'
44
import { useTheme } from '../hooks/use-theme'
55
import { useProviderStore } from '../state/provider-store'
66
import { BORDER_CHARS } from '../utils/ui-constants'
7-
import { PROVIDER_DEFINITIONS } from '@levelcode/common/providers/provider-registry'
7+
import { getProviderDefinition } from '@levelcode/common/providers/provider-registry'
88

99
import type { KeyEvent } from '@opentui/core'
10-
import type { ModelCatalogEntry } from '@levelcode/common/providers/provider-types'
1110

1211
interface ModelPickerProps {
1312
onClose: () => void
@@ -18,53 +17,34 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ onClose }) => {
1817

1918
const [searchQuery, setSearchQuery] = useState('')
2019
const [selectedIndex, setSelectedIndex] = useState(0)
21-
const [selectedProviderId, setSelectedProviderId] = useState<string | null>(null)
2220

2321
const catalogModels = useProviderStore((state) => state.catalogModels)
2422
const activeModel = useProviderStore((state) => state.config.activeModel)
2523
const activeProvider = useProviderStore((state) => state.config.activeProvider)
26-
const configuredProviders = useProviderStore(
27-
(state) => Object.keys(state.config.providers),
28-
)
2924

3025
const filteredModels = useMemo(() => {
31-
let models = catalogModels
32-
33-
// Filter by selected provider
34-
if (selectedProviderId) {
35-
models = models.filter((m) => m.providerId === selectedProviderId)
36-
}
37-
38-
// Filter by search query
39-
if (searchQuery.trim()) {
40-
const query = searchQuery.toLowerCase()
41-
models = models.filter(
42-
(m) =>
43-
m.name.toLowerCase().includes(query) ||
44-
m.id.toLowerCase().includes(query),
45-
)
46-
}
47-
48-
return models
49-
}, [catalogModels, selectedProviderId, searchQuery])
50-
51-
const formatCost = (model: ModelCatalogEntry): string => {
52-
if (!model.cost) return '-'
53-
return `$${model.cost.input.toFixed(2)}/$${model.cost.output.toFixed(2)}`
54-
}
55-
56-
const formatContext = (model: ModelCatalogEntry): string => {
57-
if (!model.limit?.context) return '-'
58-
const ctx = model.limit.context
59-
if (ctx >= 1_000_000) return `${(ctx / 1_000_000).toFixed(1)}M`
60-
if (ctx >= 1_000) return `${Math.round(ctx / 1_000)}K`
61-
return String(ctx)
62-
}
63-
64-
const getProviderName = (providerId: string): string => {
65-
const def = PROVIDER_DEFINITIONS[providerId]
66-
return def?.name ?? providerId
67-
}
26+
if (!searchQuery.trim()) return catalogModels
27+
const query = searchQuery.toLowerCase()
28+
return catalogModels.filter(
29+
(m) =>
30+
m.name.toLowerCase().includes(query) ||
31+
m.id.toLowerCase().includes(query),
32+
)
33+
}, [catalogModels, searchQuery])
34+
35+
// Clamp selectedIndex
36+
const clampedIndex = Math.min(selectedIndex, Math.max(0, filteredModels.length - 1))
37+
38+
// Visible window
39+
const maxVisible = 10
40+
const scrollOffset = Math.max(
41+
0,
42+
Math.min(clampedIndex - Math.floor(maxVisible / 2), filteredModels.length - maxVisible),
43+
)
44+
const visibleModels = filteredModels.slice(
45+
Math.max(0, scrollOffset),
46+
Math.max(0, scrollOffset) + maxVisible,
47+
)
6848

6949
useKeyboard(
7050
useCallback(
@@ -73,82 +53,39 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ onClose }) => {
7353
onClose()
7454
return
7555
}
76-
7756
if (key.name === 'up') {
7857
setSelectedIndex((prev) => Math.max(0, prev - 1))
7958
return
8059
}
8160
if (key.name === 'down') {
82-
setSelectedIndex((prev) =>
83-
Math.min(filteredModels.length - 1, prev + 1),
84-
)
61+
setSelectedIndex((prev) => Math.min(filteredModels.length - 1, prev + 1))
8562
return
8663
}
87-
8864
if (key.name === 'return' || key.name === 'enter') {
89-
const model = filteredModels[selectedIndex]
65+
const model = filteredModels[clampedIndex]
9066
if (model) {
91-
useProviderStore
92-
.getState()
93-
.setActiveModel(model.providerId, model.id)
94-
onClose()
95-
}
96-
return
97-
}
98-
99-
// Tab cycles through provider filters
100-
if (key.name === 'tab') {
101-
if (!selectedProviderId) {
102-
setSelectedProviderId(configuredProviders[0] ?? null)
103-
} else {
104-
const currentIdx = configuredProviders.indexOf(selectedProviderId)
105-
const nextIdx = currentIdx + 1
106-
if (nextIdx >= configuredProviders.length) {
107-
setSelectedProviderId(null) // cycle back to "all"
108-
} else {
109-
setSelectedProviderId(configuredProviders[nextIdx] ?? null)
110-
}
67+
useProviderStore.getState().setActiveModel(model.providerId, model.id)
11168
}
112-
setSelectedIndex(0)
69+
onClose()
11370
return
11471
}
115-
116-
// Backspace in search
11772
if (key.name === 'backspace' || key.name === 'delete') {
11873
setSearchQuery((prev) => prev.slice(0, -1))
11974
setSelectedIndex(0)
12075
return
12176
}
122-
123-
// Printable character input for search
12477
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
12578
setSearchQuery((prev) => prev + key.sequence)
12679
setSelectedIndex(0)
12780
return
12881
}
12982
},
130-
[filteredModels, selectedIndex, selectedProviderId, configuredProviders, onClose],
83+
[filteredModels, clampedIndex, onClose],
13184
),
13285
)
13386

134-
// Determine visible range for scrolling
135-
const maxVisible = 12
136-
const scrollOffset = Math.max(
137-
0,
138-
Math.min(
139-
selectedIndex - Math.floor(maxVisible / 2),
140-
filteredModels.length - maxVisible,
141-
),
142-
)
143-
const visibleModels = filteredModels.slice(
144-
scrollOffset,
145-
scrollOffset + maxVisible,
146-
)
147-
14887
return (
14988
<box
150-
title=" Model Picker "
151-
titleAlignment="center"
15289
style={{
15390
width: '100%',
15491
borderStyle: 'single',
@@ -159,83 +96,42 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ onClose }) => {
15996
flexDirection: 'column',
16097
}}
16198
>
162-
{/* Search bar */}
163-
<box style={{ flexDirection: 'row', height: 1 }}>
164-
<text style={{ fg: theme.primary }}>Search: </text>
165-
<text style={{ fg: theme.foreground }}>
166-
{searchQuery || ''}
167-
</text>
168-
<text style={{ fg: theme.muted }}>
169-
{searchQuery ? '' : '(type to filter)'}
170-
</text>
171-
</box>
172-
173-
{/* Provider filter indicator */}
174-
<box style={{ flexDirection: 'row', height: 1 }}>
175-
<text style={{ fg: theme.muted }}>
176-
Filter:{' '}
177-
</text>
178-
<text
179-
style={{
180-
fg: selectedProviderId ? theme.info : theme.muted,
181-
}}
182-
>
183-
{selectedProviderId
184-
? getProviderName(selectedProviderId)
185-
: 'All providers'}
186-
</text>
187-
<text style={{ fg: theme.muted }}>
188-
{' '}({filteredModels.length} models)
189-
</text>
190-
</box>
191-
192-
{/* Column header */}
99+
<text style={{ fg: theme.primary }}>
100+
{'Model Picker'}
101+
</text>
193102
<text style={{ fg: theme.muted }}>
194-
{' '}{'Name'.padEnd(32)}{'Provider'.padEnd(16)}{'Cost'.padEnd(16)}{'Context'}
103+
{'Search: '}{searchQuery || '(type to filter)'}
104+
</text>
105+
<text style={{ fg: theme.muted }}>
106+
{filteredModels.length}{' models available'}
195107
</text>
196-
197-
{/* Model list */}
198108
{visibleModels.map((model, visibleIdx) => {
199-
const realIndex = scrollOffset + visibleIdx
200-
const isSelected = realIndex === selectedIndex
201-
const isActive =
202-
model.id === activeModel && model.providerId === activeProvider
109+
const realIndex = Math.max(0, scrollOffset) + visibleIdx
110+
const isSelected = realIndex === clampedIndex
111+
const isActive = model.id === activeModel && model.providerId === activeProvider
112+
const providerName = getProviderDefinition(model.providerId)?.name ?? model.providerId
203113

204114
return (
205-
<box key={`${model.providerId}-${model.id}`} style={{ flexDirection: 'row', height: 1 }}>
206-
<text
207-
style={{
208-
fg: isActive
209-
? theme.success
210-
: isSelected
211-
? theme.primary
212-
: theme.foreground,
213-
bg: isSelected ? theme.surface : undefined,
214-
}}
215-
>
216-
{isSelected ? '> ' : ' '}
217-
{model.name.slice(0, 30).padEnd(30)}
218-
{' '}
219-
{getProviderName(model.providerId).slice(0, 14).padEnd(14)}
220-
{' '}
221-
{formatCost(model).padEnd(14)}
222-
{' '}
223-
{formatContext(model)}
224-
{isActive ? ' *' : ''}
225-
</text>
226-
</box>
115+
<text
116+
key={`${model.providerId}-${model.id}-${visibleIdx}`}
117+
style={{
118+
fg: isActive ? theme.success : isSelected ? theme.primary : theme.foreground,
119+
}}
120+
>
121+
{isSelected ? '> ' : ' '}
122+
{model.name.slice(0, 30).padEnd(32)}
123+
{providerName.slice(0, 14).padEnd(16)}
124+
{isActive ? '*' : ''}
125+
</text>
227126
)
228127
})}
229-
230128
{filteredModels.length === 0 && (
231129
<text style={{ fg: theme.muted }}>
232-
{' '}No models found.
130+
{' No models found. Run /provider:add first.'}
233131
</text>
234132
)}
235-
236-
{/* Help text */}
237133
<text style={{ fg: theme.muted }}>
238-
{'\u2191\u2193 navigate \u00B7 Enter select \u00B7 Tab filter provider \u00B7 Esc close'}
134+
{'Up/Down navigate | Enter select | Esc close'}
239135
</text>
240136
</box>
241137
)

0 commit comments

Comments
 (0)