Inline ListChangedIndicator with titles, normalize screen gaps, and rename ResultPanel to ToolResultPanel

- Place ListChangedIndicator on same line as section titles using Group with space-between in ToolControls, ResourceControls, and PromptControls
- Add ListChanged stories for ResourceControls and show indicators in ConnectedView stories
- Normalize ScreenLayout gap from "xl" to "md" across all screens for consistent spacing
- Rename ResultPanel to ToolResultPanel (component, interface, files, and stories)
- Replace Grid layout in ToolsScreen with equal-width flex children for detail and result panels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-01 12:47:41 -04:00
commit 5594e940c9
7 changed files with 69 additions and 56 deletions

View file

@ -25,7 +25,6 @@ function formatPlaceholder(name: string): string {
}
export function PromptArgumentsForm({
name,
description,
arguments: promptArguments,
argumentValues,
@ -34,7 +33,7 @@ export function PromptArgumentsForm({
}: PromptArgumentsFormProps) {
return (
<Stack gap="md">
<Title order={4}>{name}</Title>
<Title order={4}>Prompt Arguments</Title>
{description && <DescriptionText>{description}</DescriptionText>}
{promptArguments.length > 0 && (
<Stack gap="sm">

View file

@ -1,4 +1,4 @@
import { ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import { Group, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { PromptListItem } from "../PromptListItem/PromptListItem";
import type { PromptItem } from "../../screens/PromptsScreen/PromptsScreen";
@ -30,8 +30,10 @@ export function PromptControls({
return (
<Stack gap="sm">
<Title order={4}>Prompts</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
<Group justify="space-between">
<Title order={4}>Prompts</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
</Group>
<TextInput
placeholder="Search prompts..."
value={searchText}

View file

@ -63,6 +63,13 @@ export const WithSearch: Story = {
},
};
export const ListChanged: Story = {
args: {
...Default.args,
listChanged: true,
},
};
export const Empty: Story = {
args: {
resources: [],

View file

@ -1,5 +1,12 @@
import { useState } from "react";
import { Accordion, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import {
Accordion,
Group,
ScrollArea,
Stack,
TextInput,
Title,
} from "@mantine/core";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ResourceListItem } from "../ResourceListItem/ResourceListItem";
import { ResourceSubscribedItem } from "../ResourceSubscribedItem/ResourceSubscribedItem";
@ -68,8 +75,10 @@ export function ResourceControls({
return (
<Stack gap="sm">
<Title order={4}>Resources</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
<Group justify="space-between">
<Title order={4}>Resources</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
</Group>
<TextInput
placeholder="Search..."
value={searchText}

View file

@ -1,4 +1,4 @@
import { ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import { Group, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ToolListItem } from "../ToolListItem/ToolListItem";
import type { ToolListItemProps } from "../ToolListItem/ToolListItem";
@ -32,8 +32,10 @@ export function ToolControls({
return (
<Stack gap="sm">
<Title order={4}>Tools</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
<Group justify="space-between">
<Title order={4}>Tools</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
</Group>
<TextInput
placeholder="Search tools..."
value={searchText}

View file

@ -0,0 +1,70 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ToolResultPanel } from "./ToolResultPanel";
const meta: Meta<typeof ToolResultPanel> = {
title: "Groups/ToolResultPanel",
component: ToolResultPanel,
args: {
onClear: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ToolResultPanel>;
export const Empty: Story = {
args: {
content: [],
},
};
export const TextResult: Story = {
args: {
content: [
{
type: "text",
text: "The current weather in San Francisco is 65°F and sunny.",
},
],
},
};
export const JsonResult: Story = {
args: {
content: [
{
type: "json",
text: '{"temperature":65,"unit":"fahrenheit","condition":"sunny","city":"San Francisco"}',
},
],
},
};
export const ImageResult: Story = {
args: {
content: [
{
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
mimeType: "image/png",
},
],
},
};
export const MixedContent: Story = {
args: {
content: [
{
type: "text",
text: "Here is the generated image based on your description:",
},
{
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
mimeType: "image/png",
},
],
},
};

View file

@ -0,0 +1,67 @@
import { Button, Group, Stack, Text, Title } from "@mantine/core";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
export interface ResultContentItem {
type: string;
text?: string;
data?: string;
mimeType?: string;
}
export interface ToolResultPanelProps {
content: ResultContentItem[];
onClear: () => void;
}
const ClearButton = Button.withProps({
variant: "light",
size: "sm",
});
function resolveTextType(type: string): "text" | "json" {
return type === "text" ? "text" : "json";
}
function resolveMediaType(mimeType?: string): "image" | "audio" {
return mimeType?.startsWith("image") ? "image" : "audio";
}
export function ToolResultPanel({ content, onClear }: ToolResultPanelProps) {
return (
<Stack>
<Title order={4}>Results</Title>
{content.length === 0 ? (
<Text c="dimmed">No results yet</Text>
) : (
<>
{content.map((item, index) => {
if (item.text !== undefined) {
return (
<ContentViewer
key={index}
type={resolveTextType(item.type)}
content={item.text}
copyable
/>
);
}
if (item.data !== undefined) {
return (
<ContentViewer
key={index}
type={resolveMediaType(item.mimeType)}
content={item.data}
mimeType={item.mimeType}
/>
);
}
return null;
})}
</>
)}
<Group>
<ClearButton onClick={onClear}>Clear</ClearButton>
</Group>
</Stack>
);
}