Skip to content

Commit f7f62c3

Browse files
committed
fix(web): align the MCP server form and the provider dialog corners
The form used an auto-fit grid, so on a wide pane the five fields packed four to a row, mixed short inputs with taller textareas, and left the last row three columns empty. Name and Transport now share the first row and every longer field spans both columns, with the grid capped so the inputs stop stretching. The provider dialog rounded its border but did not clip its children, so the header background, the footer, and the list scrollbar painted square over the top corners. The footer worked around this with its own radius; clipping at the dialog covers all three.
1 parent cb2ecdc commit f7f62c3

5 files changed

Lines changed: 71 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Lay the MCP server form out in even rows instead of a ragged grid, and stop the provider manager header and footer from squaring off the dialog corners.

‎apps/pythinker-web/src/components/ProviderManager.vue‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,9 @@ function statusLabel(status: AppProvider['status']): string {
353353
max-height: calc(100vh - 80px);
354354
display: flex;
355355
flex-direction: column;
356+
/* The header, the footer, and the list scrollbar all paint to the dialog
357+
edge, so they square off the rounded corners without this. */
358+
overflow: hidden;
356359
font-family: var(--mono);
357360
box-shadow: 0 8px 32px rgba(0,0,0,0.14);
358361
}
@@ -578,7 +581,6 @@ function statusLabel(status: AppProvider['status']): string {
578581
color: var(--faint);
579582
border-top: 1px solid var(--line2);
580583
background: var(--panel);
581-
border-radius: 0 0 4px 4px;
582584
}
583585
584586
@media (max-width: 640px) {

‎apps/pythinker-web/src/components/settings/McpServerForm.vue‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,23 +99,23 @@ function submit(): void {
9999
<option value="sse">sse</option>
100100
</select>
101101
</label>
102-
<label v-if="form.transport === 'stdio'" class="connector-field">
102+
<label v-if="form.transport === 'stdio'" class="connector-field connector-field-wide">
103103
<span class="rlabel">{{ t('settings.connectors.form.command') }}</span>
104104
<input v-model="form.command" class="page-search" required />
105105
</label>
106-
<label v-if="form.transport === 'stdio'" class="connector-field">
106+
<label v-if="form.transport === 'stdio'" class="connector-field connector-field-wide">
107107
<span class="rlabel">{{ t('settings.connectors.form.args') }}</span>
108108
<textarea v-model="form.args" class="page-search connector-textarea" :placeholder="t('settings.connectors.form.argsHint')" />
109109
</label>
110-
<label v-if="form.transport === 'stdio'" class="connector-field">
110+
<label v-if="form.transport === 'stdio'" class="connector-field connector-field-wide">
111111
<span class="rlabel">{{ t('settings.connectors.form.env') }}</span>
112112
<textarea v-model="form.env" class="page-search connector-textarea" :placeholder="t('settings.connectors.form.objectHint')" />
113113
</label>
114-
<label v-else class="connector-field">
114+
<label v-else class="connector-field connector-field-wide">
115115
<span class="rlabel">{{ t('settings.connectors.form.url') }}</span>
116116
<input v-model="form.url" class="page-search" type="url" required />
117117
</label>
118-
<label v-if="form.transport !== 'stdio'" class="connector-field">
118+
<label v-if="form.transport !== 'stdio'" class="connector-field connector-field-wide">
119119
<span class="rlabel">{{ t('settings.connectors.form.headers') }}</span>
120120
<textarea v-model="form.headers" class="page-search connector-textarea" :placeholder="t('settings.connectors.form.objectHint')" />
121121
</label>
@@ -139,11 +139,16 @@ function submit(): void {
139139
background: var(--panel);
140140
}
141141
.connector-fields {
142+
/* Two fixed columns, not auto-fit: a pane-wide grid stretches the short
143+
fields and leaves a ragged tail row. Name and Transport share the first
144+
row; every longer field spans both columns so no row ends half empty. */
142145
display: grid;
143-
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
146+
grid-template-columns: repeat(2, minmax(0, 1fr));
144147
gap: 8px 12px;
148+
max-width: 640px;
145149
}
146150
.connector-field { min-width: 0; }
151+
.connector-field-wide { grid-column: 1 / -1; }
147152
.connector-field .page-search { margin: 4px 0 0; }
148153
.connector-textarea {
149154
min-height: 64px;
@@ -155,4 +160,7 @@ function submit(): void {
155160
gap: 8px;
156161
margin-top: 4px;
157162
}
163+
@media (max-width: 640px) {
164+
.connector-fields { grid-template-columns: 1fr; }
165+
}
158166
</style>

‎apps/pythinker-web/test/connectors-page.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,23 @@ describe('ConnectorsPage', () => {
8787
expect(wrapper.findAll('.connector-remove')).toHaveLength(1);
8888
});
8989

90+
it('spans every long field across both form columns', async () => {
91+
const wrapper = mountPage();
92+
93+
await wrapper.get('button.act').trigger('click');
94+
const labelOf = (field: ReturnType<typeof wrapper.get>): string => field.get('.rlabel').text();
95+
const wide = wrapper.findAll('.connector-field-wide').map(labelOf);
96+
const narrow = wrapper
97+
.findAll('.connector-field')
98+
.filter((field) => !field.classes().includes('connector-field-wide'))
99+
.map(labelOf);
100+
101+
// Name and Transport pair up on the first row; the rest take a full row,
102+
// so no row is left half empty.
103+
expect(narrow).toEqual(['Name', 'Transport']);
104+
expect(wide).toEqual(['Command', 'Arguments', 'Environment (JSON)']);
105+
});
106+
90107
it('shows the daemon validation message after a rejected write', () => {
91108
const wrapper = mountPage({ connectorsError: 'MCP server id must be trimmed' });
92109

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { readFileSync } from 'node:fs';
2+
import { resolve } from 'node:path';
3+
4+
import { describe, expect, it } from 'vitest';
5+
6+
// The header, the footer, and the provider list all paint their own background
7+
// to the dialog edge, so a rounded dialog needs to clip them. jsdom applies no
8+
// scoped-SFC styles, so the rule itself is the contract under test.
9+
const source = readFileSync(
10+
resolve(import.meta.dirname, '../src/components/ProviderManager.vue'),
11+
'utf8',
12+
);
13+
14+
function blockOf(selector: string): string {
15+
const start = source.indexOf(`\n${selector} {`);
16+
expect(start).toBeGreaterThan(-1);
17+
const end = source.indexOf('\n}', start);
18+
return source.slice(start, end);
19+
}
20+
21+
describe('ProviderManager dialog chrome', () => {
22+
it('clips its children to the rounded corners', () => {
23+
const dialog = blockOf('.dialog');
24+
25+
expect(dialog).toMatch(/border-radius:\s*4px/);
26+
expect(dialog).toMatch(/overflow:\s*hidden/);
27+
});
28+
29+
it('leaves the corner rounding to the dialog instead of the footer', () => {
30+
expect(blockOf('.footer-hint')).not.toMatch(/border-radius/);
31+
});
32+
});

0 commit comments

Comments
 (0)