feat(web): add Mantine clear buttons to text inputs app-wide (#1333) (#1448)

* feat(web): add Mantine clear buttons to text inputs app-wide (#1333)

Add a clear (×) affordance to every user-editable text input across the
web client so a populated field can be reset in one click.

- Autocomplete: enable `clearable` by default via the theme
  (`theme/Autocomplete.ts`) — covers the prompt-argument and
  resource-template variable autocomplete branches.
- TextInput / Textarea: Mantine v8 does not expose a public `clearable`
  prop on these primitives (only the combobox-based components do), so
  add the clear button manually via `rightSection` + `CloseButton`
  (aria-label="Clear") shown only when the field has a value, wired to
  reset the field's controlled value to "".

Applied to sidebar search inputs (prompts, tools, apps, tasks, history,
resources, network, logs), prompt-argument and resource-template
variable inputs, the sampling response/model fields, server config &
settings forms, roots table, schema form, experimental features panel,
and the server.json import panel. Read-only/display Textareas
(OutputValidationModal, UrlElicitationErrorModal) are intentionally left
untouched.

Tests cover the clear behavior on representative inputs from each
category (sidebar search, prompt argument, resource-template variable,
sampling response, plus the config/settings forms).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): disable ServerConfigModal clear buttons while submitting

Address review #2 on #1448: the clear CloseButton sat in `rightSection`
with `rightSectionPointerEvents="auto"`, so it stayed clickable even
while the field was `disabled={submitting}`, letting a user wipe a field
mid-submit. Forward `disabled={submitting}` to each clear button so it
locks together with its input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(web): remove redundant Auto-respond from SamplingRequestPanel

The sampling modal pre-loads its draft with the default stub result, and
"Send Response" sends that draft — so on an untouched form "Auto-respond"
did exactly the same thing as "Send Response", just with a fresh copy of
the same defaults. Drop the redundant button and its `onAutoRespond` prop;
"Send Response" now covers both the unedited (stub) and edited cases.

The InlineSamplingRequest variant keeps its Auto-respond button — there it
is the quick path versus "Edit & Send" (which opens this editable modal),
not a duplicate of an adjacent send action.

Updated PendingClientRequestModal wiring, stories, and tests accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-06-12 09:51:05 -04:00 • committed by GitHub
commit 0890c7509a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 805 additions and 28 deletions

View file

@ -196,6 +196,40 @@ describe("ImportServerJsonPanel", () => {
expect(onServerNameChange).toHaveBeenCalledWith("X");
});
it("clears the paste textarea, env-var, and name-override fields via Clear buttons", async () => {
const user = userEvent.setup();
const onJsonChange = vi.fn();
const onEnvVarChange = vi.fn();
const onServerNameChange = vi.fn();
const envVars: EnvVarInfo[] = [
{ name: "DEBUG", required: false, value: "true" },
];
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
onJsonChange={onJsonChange}
onEnvVarChange={onEnvVarChange}
onServerNameChange={onServerNameChange}
draft={{
rawText: "{}",
envOverrides: {},
nameOverride: "Custom Name",
}}
validation={[]}
envVars={envVars}
/>,
);
// DOM order: paste textarea, env-var input, name-override input.
const clearButtons = screen.getAllByRole("button", { name: "Clear" });
expect(clearButtons).toHaveLength(3);
await user.click(clearButtons[0]);
expect(onJsonChange).toHaveBeenCalledWith("");
await user.click(clearButtons[1]);
expect(onEnvVarChange).toHaveBeenCalledWith("DEBUG", "");
await user.click(clearButtons[2]);
expect(onServerNameChange).toHaveBeenCalledWith("");
});
it("renders the existing nameOverride value", () => {
renderWithMantine(
<ImportServerJsonPanel

View file

@ -1,5 +1,6 @@
import {
Button,
CloseButton,
Divider,
Group,
Radio,
@ -88,6 +89,12 @@ export function ImportServerJsonPanel({
autosize
minRows={8}
maxRows={15}
rightSectionPointerEvents="auto"
rightSection={
draft.rawText ? (
<CloseButton aria-label="Clear" onClick={() => onJsonChange("")} />
) : null
}
/>
<Divider />
@ -139,6 +146,15 @@ export function ImportServerJsonPanel({
onChange={(e) =>
onEnvVarChange(envVar.name, e.currentTarget.value)
}
rightSectionPointerEvents="auto"
rightSection={
envVar.value ? (
<CloseButton
aria-label="Clear"
onClick={() => onEnvVarChange(envVar.name, "")}
/>
) : null
}
/>
))}
</>
@ -150,6 +166,15 @@ export function ImportServerJsonPanel({
label="Server Name (optional override)"
value={draft.nameOverride ?? ""}
onChange={(e) => onServerNameChange(e.currentTarget.value)}
rightSectionPointerEvents="auto"
rightSection={
draft.nameOverride ? (
<CloseButton
aria-label="Clear"
onClick={() => onServerNameChange("")}
/>
) : null
}
/>
<Group justify="flex-end">