* feat(web): single header MonitoringToggle + Prompts-style Tools results (#1661) Consolidate the monitoring column's open/close affordances into one header toggle, and align the Tools/Prompts result panes and the sidebar log layout. - Add a single `MonitoringToggle` in the header, to the right of the theme icon. It opens/closes the monitoring column and only shows when a server is connected OR a connect attempt failed (wide viewport + a monitor tab available) — the same availability rule the column uses. - Remove the scattered pin/close icons: delete `PinColumnButton`; drop `onPin` from the Logs/Protocol/Network/Console screens and their stream panels; drop `onOpenMonitor` from the server list; remove the close button from `MonitoringControls`/`MonitoringScreen`. - Tools screen: remove the "Results will appear here" placeholder pane. Results now replace the input form in a single content pane (the Prompts pattern), with a top-left X (ToolResultPanel) that dismisses them back to the form; form values are preserved for a re-run. - Prompts screen: the argument form fills the content pane width (drop the 40% cap), matching the Tools input. - LogEntry: add a `compact` two-line layout (time/level/logger on line one, message wrapping below) used by the embedded monitoring-column Logs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95 * polish(web): tooltips on icon buttons + rename "monitoring column" → "monitoring sidebar" (#1661) - Add Mantine `Tooltip`s to the icon-only controls: Client settings, Theme (dynamic light/dark label), the monitoring-sidebar toggle, the list-level and per-entry Expand/Collapse toggles, the Pin/Unpin toggle, and the Replay button. Labels mirror each control's existing aria-label. - Rename the feature term "monitoring column" → "monitoring sidebar" across the user-facing labels (Open/Close/Resize), comments, and tests, so the UI calls it a sidebar consistently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95 * review: consistent sidebar wording + reuse MonitoringToggleProps (#1661) Address the two optional notes from the PR #1662 review: - Make the MonitoringToggle JSDoc voice consistent ("sidebar" throughout, no lingering "column"). - Drop ViewHeader's duplicate `MonitorToggleProps` and reuse the element's exported `MonitoringToggleProps` as the single source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95 * feat(web): slide-in/out animation for the monitoring-sidebar toggle (#1661) The header MonitoringToggle now animates in/out via a Mantine Transition whose in/out styles drive width + margin-left (plus opacity/translateX): as its slot grows from 0 → the 36px icon, the flex-end header row reflows and pushes the theme + client-settings buttons one slot to the left; the exit reverses and they slide back over. A negative margin-left while collapsed cancels the row gap so the theme icon stays flush-right when the toggle is absent. Only appearance/ disappearance (connect / disconnect / narrow viewport) animates — opening or closing the sidebar leaves the toggle in place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4e8e772aff
commit
dbda70e83b
12 changed files with 613 additions and 440 deletions
|
|
@ -100,15 +100,6 @@ describe("ConsoleScreen", () => {
|
|||
expect(onExport).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("shows a pin button only when onPin is provided", () => {
|
||||
const { rerender } = renderWithMantine(<ConsoleScreen {...makeProps()} />);
|
||||
expect(screen.queryByRole("button", { name: "Pin as column" })).toBeNull();
|
||||
rerender(<ConsoleScreen {...makeProps({ onPin: vi.fn() })} />);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Pin as column" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("drops the search sidebar when embedded", () => {
|
||||
renderWithMantine(<ConsoleScreen {...makeProps({ embedded: true })} />);
|
||||
// The embedded column has no in-screen search box (the column supplies it).
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {
|
|||
SortToggle,
|
||||
type SortDirection,
|
||||
} from "../../elements/SortToggle/SortToggle";
|
||||
import { PinColumnButton } from "../../elements/PinColumnButton/PinColumnButton";
|
||||
import { EmbeddableScrollArea } from "../../elements/EmbeddableScrollArea/EmbeddableScrollArea";
|
||||
import { useScrollMemory } from "../../../hooks/useScrollMemory";
|
||||
|
||||
|
|
@ -28,8 +27,6 @@ export interface ConsoleScreenProps {
|
|||
onExport: () => void;
|
||||
sortDirection: SortDirection;
|
||||
onSortChange: (next: SortDirection) => void;
|
||||
/** See LoggingScreen: shows a "pin as column" button when set (#1616). */
|
||||
onPin?: () => void;
|
||||
/** See LoggingScreen: fills the parent height and drops the filter sidebar. */
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
|
@ -112,7 +109,6 @@ export function ConsoleScreen({
|
|||
onExport,
|
||||
sortDirection,
|
||||
onSortChange,
|
||||
onPin,
|
||||
embedded = false,
|
||||
}: ConsoleScreenProps) {
|
||||
const { filterText } = ui;
|
||||
|
|
@ -176,7 +172,6 @@ export function ConsoleScreen({
|
|||
>
|
||||
Export
|
||||
</Button>
|
||||
{onPin ? <PinColumnButton onPin={onPin} /> : null}
|
||||
</Group>
|
||||
</Group>
|
||||
{filteredEntries.length > 0 ? (
|
||||
|
|
|
|||
|
|
@ -132,14 +132,6 @@ describe("LoggingScreen", () => {
|
|||
expect(onSetLevel).toHaveBeenCalledWith("info");
|
||||
});
|
||||
|
||||
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onPin = vi.fn();
|
||||
renderWithMantine(<LoggingScreen {...baseProps} onPin={onPin} />);
|
||||
await user.click(screen.getByRole("button", { name: "Pin as column" }));
|
||||
expect(onPin).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("drops the filter sidebar when embedded, keeping the stream", () => {
|
||||
renderWithMantine(<LoggingScreen {...baseProps} embedded />);
|
||||
expect(screen.getByText("Log Stream")).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@ export interface LoggingScreenProps {
|
|||
sortDirection: SortDirection;
|
||||
onSortChange: (next: SortDirection) => void;
|
||||
/**
|
||||
* When set, the stream panel shows a "pin as column" button (#1616). Omitted
|
||||
* when already embedded, or when pinning isn't currently available.
|
||||
*/
|
||||
onPin?: () => void;
|
||||
/**
|
||||
* True when rendered inside the monitoring column: the screen fills its
|
||||
* True when rendered inside the monitoring sidebar: the screen fills its
|
||||
* parent's height (instead of the viewport calc) and drops the filter
|
||||
* sidebar so the narrow column is stream-only.
|
||||
*/
|
||||
|
|
@ -63,7 +58,6 @@ export function LoggingScreen({
|
|||
onExport,
|
||||
sortDirection,
|
||||
onSortChange,
|
||||
onPin,
|
||||
embedded = false,
|
||||
}: LoggingScreenProps) {
|
||||
const { filterText, visibleLevels } = ui;
|
||||
|
|
@ -110,7 +104,6 @@ export function LoggingScreen({
|
|||
onExport={onExport}
|
||||
sortDirection={sortDirection}
|
||||
onSortChange={onSortChange}
|
||||
onPin={onPin}
|
||||
embedded={embedded}
|
||||
/>
|
||||
</ScreenLayout>
|
||||
|
|
|
|||
|
|
@ -164,14 +164,6 @@ describe("NetworkScreen", () => {
|
|||
expect(screen.getByRole("button", { name: "Export" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onPin = vi.fn();
|
||||
renderWithMantine(<NetworkScreen {...baseProps} onPin={onPin} />);
|
||||
await user.click(screen.getByRole("button", { name: "Pin as column" }));
|
||||
expect(onPin).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("drops the filter sidebar when embedded, keeping the request list", () => {
|
||||
renderWithMantine(<NetworkScreen {...baseProps} embedded />);
|
||||
// The sidebar (NetworkControls, with its Search box) is not rendered.
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ export interface NetworkScreenProps {
|
|||
onSortChange: (next: SortDirection) => void;
|
||||
compact: boolean;
|
||||
onToggleCompact: () => void;
|
||||
/** See LoggingScreen: shows a "pin as column" button when set (#1616). */
|
||||
onPin?: () => void;
|
||||
/** See LoggingScreen: fills the parent height and drops the filter sidebar. */
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
|
@ -61,7 +59,6 @@ export function NetworkScreen({
|
|||
onSortChange,
|
||||
compact,
|
||||
onToggleCompact,
|
||||
onPin,
|
||||
embedded = false,
|
||||
}: NetworkScreenProps) {
|
||||
const { filterText, visibleCategories } = ui;
|
||||
|
|
@ -113,7 +110,6 @@ export function NetworkScreen({
|
|||
onSortChange={onSortChange}
|
||||
compact={compact}
|
||||
onToggleCompact={onToggleCompact}
|
||||
onPin={onPin}
|
||||
embedded={embedded}
|
||||
/>
|
||||
</ScreenLayout>
|
||||
|
|
|
|||
|
|
@ -265,10 +265,10 @@ export function PromptsScreen({
|
|||
// the resource preview layout (see ResourcesScreen).
|
||||
<PreviewPane mah={SCROLL_MAX_HEIGHT}>{renderPreview()}</PreviewPane>
|
||||
) : selectedPrompt && hasArguments(selectedPrompt) ? (
|
||||
// Argument-form branch — capped at 40% width so the form doesn't
|
||||
// stretch across the viewport on wide displays. Disappears once
|
||||
// the user clicks Get Prompt and previewActive flips on.
|
||||
<PreviewPane mah={SCROLL_MAX_HEIGHT} maw="40%">
|
||||
// Argument-form branch — fills the content pane's width (like the Tools
|
||||
// input form), replaced by the result once the user clicks Get Prompt
|
||||
// and previewActive flips on.
|
||||
<PreviewPane mah={SCROLL_MAX_HEIGHT}>
|
||||
<PreviewCard>
|
||||
<PromptArgumentsForm
|
||||
prompt={selectedPrompt}
|
||||
|
|
|
|||
|
|
@ -120,14 +120,6 @@ describe("ProtocolScreen", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onPin = vi.fn();
|
||||
renderWithMantine(<ProtocolScreen {...baseProps} onPin={onPin} />);
|
||||
await user.click(screen.getByRole("button", { name: "Pin as column" }));
|
||||
expect(onPin).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("drops the filter sidebar when embedded, keeping the request list", () => {
|
||||
renderWithMantine(<ProtocolScreen {...baseProps} embedded />);
|
||||
expect(screen.getByText("Messages")).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ export interface ProtocolScreenProps {
|
|||
onSortChange: (next: SortDirection) => void;
|
||||
compact: boolean;
|
||||
onToggleCompact: () => void;
|
||||
/** See LoggingScreen: shows a "pin as column" button when set (#1616). */
|
||||
onPin?: () => void;
|
||||
/** See LoggingScreen: fills the parent height and drops the filter sidebar. */
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
|
@ -73,7 +71,6 @@ export function ProtocolScreen({
|
|||
onSortChange,
|
||||
compact,
|
||||
onToggleCompact,
|
||||
onPin,
|
||||
embedded = false,
|
||||
}: ProtocolScreenProps) {
|
||||
const { search, methodFilter, visibleDirections } = ui;
|
||||
|
|
@ -142,7 +139,6 @@ export function ProtocolScreen({
|
|||
onSortChange={onSortChange}
|
||||
compact={compact}
|
||||
onToggleCompact={onToggleCompact}
|
||||
onPin={onPin}
|
||||
embedded={embedded}
|
||||
/>
|
||||
</ScreenLayout>
|
||||
|
|
|
|||
|
|
@ -67,12 +67,6 @@ export interface ServerListScreenProps {
|
|||
onClearHighlight?: (id: string) => void;
|
||||
compact: boolean;
|
||||
onToggleCompact: () => void;
|
||||
/**
|
||||
* Open the monitoring column. Provided only when a server is connected, the
|
||||
* column can open, and it isn't already shown; forwarded to the controls to
|
||||
* reveal an open-sidebar button beside the list toggle.
|
||||
*/
|
||||
onOpenMonitor?: () => void;
|
||||
}
|
||||
|
||||
const PageContainer = Stack.withProps({
|
||||
|
|
@ -105,7 +99,6 @@ export function ServerListScreen({
|
|||
onClearHighlight,
|
||||
compact,
|
||||
onToggleCompact,
|
||||
onOpenMonitor,
|
||||
}: ServerListScreenProps) {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
|
|
@ -203,7 +196,6 @@ export function ServerListScreen({
|
|||
onImportConfig={onImportConfig}
|
||||
onImportServerJson={onImportServerJson}
|
||||
onExport={onExport}
|
||||
onOpenMonitor={onOpenMonitor}
|
||||
/>
|
||||
|
||||
<ScrollArea.Autosize
|
||||
|
|
|
|||
|
|
@ -66,7 +66,9 @@ describe("ToolsScreen", () => {
|
|||
expect(
|
||||
screen.getByText("Select a tool to view details"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Results will appear here")).toBeInTheDocument();
|
||||
// There is no separate results placeholder pane anymore (#1661) — results
|
||||
// replace the input form in the single content pane when they exist.
|
||||
expect(screen.queryByText("Results will appear here")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows the detail panel when a tool is selected", async () => {
|
||||
|
|
@ -100,10 +102,10 @@ describe("ToolsScreen", () => {
|
|||
expect(onCallTool).toHaveBeenCalledWith("gamma", { mode: "slow" }, false);
|
||||
});
|
||||
|
||||
it("renders selection and result from props (persisted across navigation)", () => {
|
||||
it("shows the result in place of the form when a selected tool has a result", () => {
|
||||
// App owns selection + result, so a remount after a tab switch re-renders
|
||||
// with both still set — the detail and result panels show without any
|
||||
// local re-selection.
|
||||
// with both still set. The result replaces the input form in the single
|
||||
// content pane (#1661): the Execute button is gone while the result shows.
|
||||
renderWithMantine(
|
||||
<ToolsScreen
|
||||
{...baseProps}
|
||||
|
|
@ -118,6 +120,47 @@ describe("ToolsScreen", () => {
|
|||
screen.queryByText("Select a tool to view details"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Results")).toBeInTheDocument();
|
||||
// The input form is hidden while the result is shown.
|
||||
expect(screen.queryByRole("button", { name: /Execute/ })).toBeNull();
|
||||
});
|
||||
|
||||
it("returns to the input form when the result is dismissed", async () => {
|
||||
// App owns both `ui` and `callState`; this host mirrors that so dismissing
|
||||
// the result (onClearResult → callState cleared) flips the single content
|
||||
// pane back to the input form, with the selection (and thus the form)
|
||||
// preserved for a re-run (#1661).
|
||||
function Host() {
|
||||
const [ui, setUi] = useState<ToolsUiState>({
|
||||
...EMPTY_TOOLS_UI,
|
||||
selectedToolName: "alpha",
|
||||
});
|
||||
const [callState, setCallState] = useState<ToolsScreenProps["callState"]>(
|
||||
{
|
||||
status: "ok",
|
||||
result: { content: [{ type: "text", text: "ok" }] },
|
||||
},
|
||||
);
|
||||
return (
|
||||
<ToolsScreen
|
||||
{...baseProps}
|
||||
ui={ui}
|
||||
onUiChange={setUi}
|
||||
callState={callState}
|
||||
onClearResult={() => setCallState(undefined)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(<Host />);
|
||||
// A result is present, so the result pane shows and the form is hidden.
|
||||
expect(screen.getByText("Results")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /Execute/ })).toBeNull();
|
||||
// Dismissing the result flips back to the input form (Execute reappears).
|
||||
await user.click(screen.getByRole("button", { name: "Close results" }));
|
||||
expect(
|
||||
await screen.findByRole("button", { name: /Execute/ }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("Results")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the result panel when callState has a result", () => {
|
||||
|
|
@ -152,7 +195,7 @@ describe("ToolsScreen", () => {
|
|||
expect(onCallTool).toHaveBeenCalledWith("gamma", { mode: "fast" }, false);
|
||||
});
|
||||
|
||||
it("invokes onClearResult when Clear is clicked on the result panel", async () => {
|
||||
it("invokes onClearResult when the result close button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClearResult = vi.fn();
|
||||
renderWithMantine(
|
||||
|
|
@ -165,7 +208,7 @@ describe("ToolsScreen", () => {
|
|||
}}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Clear" }));
|
||||
await user.click(screen.getByRole("button", { name: "Close results" }));
|
||||
expect(onClearResult).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
|
@ -243,7 +286,7 @@ describe("ToolsScreen", () => {
|
|||
}}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Clear" }));
|
||||
await user.click(screen.getByRole("button", { name: "Close results" }));
|
||||
expect(screen.getByText("Results")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -162,7 +162,19 @@ export function ToolsScreen({
|
|||
|
||||
<ContentPane mah={SCROLL_MAX_HEIGHT}>
|
||||
<ContentCard>
|
||||
{selectedTool ? (
|
||||
{callState?.result ? (
|
||||
// Results replace the input form while present, and the panel's
|
||||
// top-left X dismisses them back to the form (#1661) — the Prompts
|
||||
// screen pattern. `formValues` live in the lifted UI state, so the
|
||||
// form is restored intact for a re-run. A call in flight sets a
|
||||
// `pending` state with no `result` (App.tsx), so the executing form
|
||||
// (progress + cancel) shows until the result lands.
|
||||
<ToolResultPanel
|
||||
result={callState.result}
|
||||
onClear={() => onClearResult?.()}
|
||||
onReadResource={onReadResource}
|
||||
/>
|
||||
) : selectedTool ? (
|
||||
<ToolDetailPanel
|
||||
tool={selectedTool}
|
||||
formValues={formValues}
|
||||
|
|
@ -186,20 +198,6 @@ export function ToolsScreen({
|
|||
)}
|
||||
</ContentCard>
|
||||
</ContentPane>
|
||||
|
||||
<ContentPane mah={SCROLL_MAX_HEIGHT}>
|
||||
<ContentCard>
|
||||
{callState?.result ? (
|
||||
<ToolResultPanel
|
||||
result={callState.result}
|
||||
onClear={() => onClearResult?.()}
|
||||
onReadResource={onReadResource}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState>Results will appear here</EmptyState>
|
||||
)}
|
||||
</ContentCard>
|
||||
</ContentPane>
|
||||
</ScreenLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue