* 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>
97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import {
|
|
CloseButton,
|
|
Group,
|
|
ScrollArea,
|
|
Stack,
|
|
Text,
|
|
TextInput,
|
|
Title,
|
|
} from "@mantine/core";
|
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
|
import { AppListItem } from "../AppListItem/AppListItem";
|
|
import { useScrollMemory } from "../../../hooks/useScrollMemory";
|
|
|
|
export interface AppControlsProps {
|
|
tools: Tool[];
|
|
selectedName?: string;
|
|
// Search text is controlled by the parent (App, via AppsScreen) so it
|
|
// persists across tab navigation within a live session — see #1417.
|
|
searchText?: string;
|
|
listChanged: boolean;
|
|
onRefreshList: () => void;
|
|
onSearchChange: (value: string) => void;
|
|
onSelectApp: (name: string) => void;
|
|
}
|
|
|
|
const LIST_MAX_HEIGHT =
|
|
"calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 220px)";
|
|
|
|
const EmptyState = Text.withProps({
|
|
c: "dimmed",
|
|
ta: "center",
|
|
py: "xl",
|
|
});
|
|
|
|
export function AppControls({
|
|
tools,
|
|
selectedName,
|
|
searchText = "",
|
|
listChanged,
|
|
onRefreshList,
|
|
onSearchChange,
|
|
onSelectApp,
|
|
}: AppControlsProps) {
|
|
const viewportRef = useScrollMemory("apps-sidebar");
|
|
const query = searchText.toLowerCase();
|
|
const filteredTools = searchText
|
|
? tools.filter(
|
|
(tool) =>
|
|
tool.name.toLowerCase().includes(query) ||
|
|
(tool.title?.toLowerCase().includes(query) ?? false),
|
|
)
|
|
: tools;
|
|
|
|
return (
|
|
<Stack gap="sm">
|
|
<Group justify="space-between">
|
|
<Title order={4}>MCP Apps ({tools.length})</Title>
|
|
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
|
</Group>
|
|
<TextInput
|
|
placeholder="Search apps..."
|
|
value={searchText}
|
|
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
|
rightSectionPointerEvents="auto"
|
|
rightSection={
|
|
searchText ? (
|
|
<CloseButton
|
|
aria-label="Clear"
|
|
onClick={() => onSearchChange("")}
|
|
/>
|
|
) : null
|
|
}
|
|
/>
|
|
<ScrollArea.Autosize viewportRef={viewportRef} mah={LIST_MAX_HEIGHT}>
|
|
<Stack gap="xs">
|
|
{filteredTools.length === 0 ? (
|
|
<EmptyState>
|
|
{tools.length === 0 ? "No apps available" : "No matching apps"}
|
|
</EmptyState>
|
|
) : (
|
|
filteredTools.map((tool) => (
|
|
<AppListItem
|
|
key={tool.name}
|
|
tool={tool}
|
|
selected={tool.name === selectedName}
|
|
onClick={() => {
|
|
if (tool.name !== selectedName) onSelectApp(tool.name);
|
|
}}
|
|
/>
|
|
))
|
|
)}
|
|
</Stack>
|
|
</ScrollArea.Autosize>
|
|
</Stack>
|
|
);
|
|
}
|