Skip to content

Commit 5f51b83

Browse files
authored
fix(web): align the MCP server form and the provider dialog corners (#122)
## What Two UI alignment fixes reported from the running preview. **MCP server form (Settings → Connectors).** The field grid used `auto-fit` with a 180px minimum, so on a wide settings pane the five fields packed four to a row, put short inputs next to taller textareas, and left the final row with three empty columns. Name and Transport now share the first row; Command, Arguments, Environment (and URL/Headers for http/sse) each span both columns. The grid is capped at 640px so the inputs stop stretching across the whole pane. **Provider manager dialog.** The dialog rounded its border but never clipped its children, so the header background, the footer, and the list scrollbar all painted square corners over the rounding. The footer had worked around this with its own `border-radius`; clipping at the dialog fixes all three and the workaround is gone. ## Tests - `connectors-page.test.ts` asserts exactly which fields are narrow and which span the row. - `provider-manager-chrome.test.ts` asserts the dialog clips and that the footer no longer carries its own rounding (jsdom applies no scoped SFC styles, so the rule is the contract). Both were mutation-checked: reverting either fix fails its test. ## Verification - `pnpm run typecheck` — exit 0 - `npx vitest run apps/pythinker-web` — 70 files / 448 tests pass - `pnpm run lint` — exit 0, zero errors - Checked in the browser against a fresh build on the local preview server <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **UI Improvements** * Improved MCP server form layout with clearer two-column alignment for name and transport fields. * Expanded longer fields across the available width. * Added responsive behavior for smaller screens. * Refined provider manager dialogs with consistent rounded corners and cleaner content clipping. * **Tests** * Added coverage for connector form layout and provider manager dialog styling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent cb2ecdc commit 5f51b83

5 files changed

Lines changed: 82 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,34 @@ 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 layout = (): { narrow: string[]; wide: string[] } => ({
96+
narrow: wrapper
97+
.findAll('.connector-field')
98+
.filter((field) => !field.classes().includes('connector-field-wide'))
99+
.map(labelOf),
100+
wide: wrapper.findAll('.connector-field-wide').map(labelOf),
101+
});
102+
103+
// Name and Transport pair up on the first row; the rest take a full row,
104+
// so no row is left half empty.
105+
expect(layout()).toEqual({
106+
narrow: ['Name', 'Transport'],
107+
wide: ['Command', 'Arguments', 'Environment (JSON)'],
108+
});
109+
110+
await wrapper.get('select').setValue('http');
111+
112+
expect(layout()).toEqual({
113+
narrow: ['Name', 'Transport'],
114+
wide: ['URL', 'Headers (JSON)'],
115+
});
116+
});
117+
90118
it('shows the daemon validation message after a rejected write', () => {
91119
const wrapper = mountPage({ connectorsError: 'MCP server id must be trimmed' });
92120

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/u);
26+
expect(dialog).toMatch(/overflow:\s*hidden/u);
27+
});
28+
29+
it('leaves the corner rounding to the dialog instead of the footer', () => {
30+
expect(blockOf('.footer-hint')).not.toMatch(/border-radius/u);
31+
});
32+
});

0 commit comments

Comments
 (0)