Monitoring sidebar toggle, Prompts-style Tools results, and icon-button UI polish (#1661) (#1662)

* 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:
Cliff Hall 2026-07-11 17:30:16 -04:00 • committed by GitHub
commit dbda70e83b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 613 additions and 440 deletions

View file

@ -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();
});
});

View file

@ -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>
);
}