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
16 changed files with 69 additions and 56 deletions

View file

@ -32,7 +32,7 @@ export function ListChangedIndicator({
<Group gap="xs">
<Dot />
<UpdateLabel>List updated</UpdateLabel>
<RefreshButton onClick={onRefresh}>↻ Refresh</RefreshButton>
<RefreshButton onClick={onRefresh}>Refresh</RefreshButton>
</Group>
);
}

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

View file

@ -14,7 +14,7 @@ export interface HistoryScreenProps {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
gap: "md",
p: "xl",
});

View file

@ -29,7 +29,7 @@ const ALL_LEVELS_VISIBLE: Record<string, boolean> = {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
gap: "md",
p: "xl",
});

View file

@ -34,7 +34,7 @@ export interface PromptsScreenProps {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
gap: "md",
p: "xl",
});

View file

@ -60,7 +60,7 @@ export interface ResourcesScreenProps {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
gap: "md",
p: "xl",
});

View file

@ -13,7 +13,7 @@ export interface TasksScreenProps {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
gap: "md",
p: "xl",
});

View file

@ -3,7 +3,7 @@ import { fn } from "storybook/test";
import { ToolsScreen } from "./ToolsScreen";
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
import type { ResultPanelProps } from "../../groups/ResultPanel/ResultPanel";
import type { ToolResultPanelProps } from "../../groups/ToolResultPanel/ToolResultPanel";
const meta: Meta<typeof ToolsScreen> = {
title: "Screens/ToolsScreen",
@ -67,7 +67,7 @@ const selectedToolData: ToolDetailPanelProps = {
onCancel: fn(),
};
const resultData: ResultPanelProps = {
const resultData: ToolResultPanelProps = {
content: [
{
type: "text",

View file

@ -1,15 +1,15 @@
import { Card, Flex, Grid, Stack, Text } from "@mantine/core";
import { Card, Flex, Stack, Text } from "@mantine/core";
import { ToolControls } from "../../groups/ToolControls/ToolControls";
import { ToolDetailPanel } from "../../groups/ToolDetailPanel/ToolDetailPanel";
import { ResultPanel } from "../../groups/ResultPanel/ResultPanel";
import { ToolResultPanel } from "../../groups/ToolResultPanel/ToolResultPanel";
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
import type { ResultPanelProps } from "../../groups/ResultPanel/ResultPanel";
import type { ToolResultPanelProps } from "../../groups/ToolResultPanel/ToolResultPanel";
export interface ToolsScreenProps {
tools: ToolListItemProps[];
selectedTool?: ToolDetailPanelProps;
result?: ResultPanelProps;
result?: ToolResultPanelProps;
listChanged: boolean;
searchText: string;
onSearchChange: (text: string) => void;
@ -20,8 +20,9 @@ export interface ToolsScreenProps {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
gap: "md",
p: "xl",
align: "flex-start",
});
const Sidebar = Stack.withProps({
@ -34,10 +35,9 @@ const SidebarCard = Card.withProps({
padding: "lg",
});
const FullHeightCard = Card.withProps({
const ContentCard = Card.withProps({
withBorder: true,
padding: "lg",
h: "100%",
});
const EmptyState = Text.withProps({
@ -71,27 +71,21 @@ export function ToolsScreen({
</SidebarCard>
</Sidebar>
<Grid align="stretch" flex={1}>
<Grid.Col span={7}>
<FullHeightCard>
{selectedTool ? (
<ToolDetailPanel {...selectedTool} />
) : (
<EmptyState>Select a tool to view details</EmptyState>
)}
</FullHeightCard>
</Grid.Col>
<ContentCard flex={1}>
{selectedTool ? (
<ToolDetailPanel {...selectedTool} />
) : (
<EmptyState>Select a tool to view details</EmptyState>
)}
</ContentCard>
<Grid.Col span={5}>
<FullHeightCard>
{result ? (
<ResultPanel {...result} />
) : (
<EmptyState>Results will appear here</EmptyState>
)}
</FullHeightCard>
</Grid.Col>
</Grid>
<ContentCard flex={1}>
{result ? (
<ToolResultPanel {...result} />
) : (
<EmptyState>Results will appear here</EmptyState>
)}
</ContentCard>
</ScreenLayout>
);
}

View file

@ -230,7 +230,7 @@ export const ToolsActive: Story = {
onClear: fn(),
}}
searchText=""
listChanged={false}
listChanged={true}
onSearchChange={fn()}
onRefreshList={fn()}
onSelectTool={fn()}
@ -446,7 +446,7 @@ export const ResourcesActive: Story = {
lastUpdated: "2026-03-17T11:15:00Z",
isSubscribed: false,
}}
listChanged={false}
listChanged={true}
searchText=""
onSearchChange={fn()}
onRefreshList={fn()}
@ -513,7 +513,7 @@ export const PromptsActive: Story = {
{ role: "assistant", content: "Hola, como estas?" },
],
}}
listChanged={false}
listChanged={true}
searchText=""
onSearchChange={fn()}
onRefreshList={fn()}