Skip to content

Commit 3cdf7b3

Browse files
committed
fix(web): bound the model quick-switch dropdown to the space above its pill
The dropdown opened upward with no height limit, so a long provider model list ran past the top of the window and the models above the fold could not be reached. Measure the pill on open and cap the menu, letting it scroll.
1 parent 774aca7 commit 3cdf7b3

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

apps/pythinker-web/src/components/Composer.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,12 +735,18 @@ const hasUpload = computed(() => !!props.uploadImage);
735735
// ---------------------------------------------------------------------------
736736
737737
const dropdownOpen = ref(false);
738+
const modelPillRef = ref<HTMLElement | null>(null);
739+
const modelDropdownStyle = ref<Record<string, string>>({});
738740
const permDropdownOpen = ref(false);
739741
const toolbarRef = ref<HTMLElement | null>(null);
740742
741743
function toggleDropdown(): void {
742744
dropdownOpen.value = !dropdownOpen.value;
743745
if (dropdownOpen.value) {
746+
const rect = modelPillRef.value?.getBoundingClientRect();
747+
modelDropdownStyle.value = rect
748+
? { maxHeight: `${Math.max(160, rect.top - 4 - 12)}px` }
749+
: {};
744750
permDropdownOpen.value = false;
745751
document.addEventListener('click', onDocClick, true);
746752
} else {
@@ -1179,6 +1185,7 @@ function selectModel(modelId: string): void {
11791185
<!-- Model pill — click to open quick-switch dropdown -->
11801186
<span
11811187
v-if="status"
1188+
ref="modelPillRef"
11821189
class="model-pill"
11831190
:class="{ open: dropdownOpen }"
11841191
role="button"
@@ -1195,7 +1202,7 @@ function selectModel(modelId: string): void {
11951202
</div>
11961203

11971204
<!-- Model dropdown — current provider models + controls + more -->
1198-
<div v-if="dropdownOpen && status" class="model-dropdown" role="menu" @click.stop>
1205+
<div v-if="dropdownOpen && status" class="model-dropdown" :style="modelDropdownStyle" role="menu" @click.stop>
11991206
<!-- Starred models from other providers -->
12001207
<div v-if="starredOtherModels.length > 0" class="md-section">{{ t('status.starredModels') }}</div>
12011208
<button
@@ -1719,6 +1726,7 @@ function selectModel(modelId: string): void {
17191726
display: flex;
17201727
flex-direction: column;
17211728
gap: 1px;
1729+
overflow-y: auto;
17221730
}
17231731
17241732
.md-section {

apps/pythinker-web/test/composer.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,30 @@ describe('Composer model dropdown', () => {
319319

320320
expect(wrapper.emitted('selectModel')).toEqual([['openai/gpt-5']]);
321321
});
322+
323+
it('bounds the quick-switch dropdown to the measured space above its pill', async () => {
324+
const wrapper = mountComposer({
325+
status: { model: 'Model 0', modelId: 'pythinker/model-0', ctxUsed: 0, ctxMax: 128000, permission: 'manual' },
326+
models: Array.from({ length: 17 }, (_, index) => ({
327+
id: `pythinker/model-${index}`,
328+
provider: 'pythinker',
329+
model: `model-${index}`,
330+
displayName: `Model ${index}`,
331+
maxContextSize: 128000,
332+
})),
333+
});
334+
const pill = wrapper.get('.model-pill');
335+
const rect = vi.spyOn(pill.element, 'getBoundingClientRect');
336+
337+
rect.mockReturnValue({ top: 20 } as DOMRect);
338+
await pill.trigger('click');
339+
expect(wrapper.get('.model-dropdown').element.style.maxHeight).toBe('160px');
340+
341+
rect.mockReturnValue({ top: 300 } as DOMRect);
342+
await pill.trigger('click');
343+
await pill.trigger('click');
344+
expect(wrapper.get('.model-dropdown').element.style.maxHeight).toBe('284px');
345+
});
322346
});
323347

324348
describe('Composer context indicator', () => {

0 commit comments

Comments
 (0)