Phase 4: Move screen selection state into the screens themselves

ToolsScreen, PromptsScreen, and ResourcesScreen each tracked the user's
current selection via `selectedXxxName` / `onSelectXxx` props, hoisting
screen-local UI state into whatever rendered them. InspectorView never
threaded those callbacks anywhere, so clicks in the controls panel did
not display the matching detail panel — the detail panel only rendered
if a parent fed selection back in.

Move selection into `useState` inside each screen and drop the
`selectedXxxName` / `onSelectXxx` props. MCP-derived data (`callState`,
`getPromptState`, `readState`) keeps flowing in from `InspectorView` as
props. ResourcesScreen wraps `onReadResource` so a template-expanded URI
becomes the selected resource for preview rendering.

Update each screen's stories to drop the deleted props. Stories that
need a pre-selected item use a `play` function to click it — no
component coupling to Storybook.

Add Phase 4.3 to the interfaces plan describing the rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-26 09:27:22 -04:00
commit 4c1d2f2c34
8 changed files with 171 additions and 86 deletions

View file

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js"; import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test"; import { fn, userEvent, within } from "storybook/test";
import { PromptsScreen } from "./PromptsScreen"; import { PromptsScreen } from "./PromptsScreen";
import type { GetPromptState } from "./PromptsScreen"; import type { GetPromptState } from "./PromptsScreen";
@ -10,7 +10,6 @@ const meta: Meta<typeof PromptsScreen> = {
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
args: { args: {
onRefreshList: fn(), onRefreshList: fn(),
onSelectPrompt: fn(),
onGetPrompt: fn(), onGetPrompt: fn(),
onCopyMessages: fn(), onCopyMessages: fn(),
listChanged: false, listChanged: false,
@ -70,6 +69,11 @@ const translateResult: GetPromptState = {
}, },
}; };
async function selectPromptByName(canvasElement: HTMLElement, name: string) {
const canvas = within(canvasElement);
await userEvent.click(await canvas.findByText(name));
}
export const NoSelection: Story = { export const NoSelection: Story = {
args: { args: {
prompts: samplePrompts, prompts: samplePrompts,
@ -79,36 +83,44 @@ export const NoSelection: Story = {
export const PromptSelected: Story = { export const PromptSelected: Story = {
args: { args: {
prompts: samplePrompts, prompts: samplePrompts,
selectedPromptName: "translate", },
play: async ({ canvasElement }) => {
await selectPromptByName(canvasElement, "translate");
}, },
}; };
export const WithResult: Story = { export const WithResult: Story = {
args: { args: {
prompts: samplePrompts, prompts: samplePrompts,
selectedPromptName: "translate",
getPromptState: translateResult, getPromptState: translateResult,
}, },
play: async ({ canvasElement }) => {
await selectPromptByName(canvasElement, "translate");
},
}; };
export const Loading: Story = { export const Loading: Story = {
args: { args: {
prompts: samplePrompts, prompts: samplePrompts,
selectedPromptName: "translate",
getPromptState: { status: "pending" }, getPromptState: { status: "pending" },
}, },
play: async ({ canvasElement }) => {
await selectPromptByName(canvasElement, "translate");
},
}; };
export const WithError: Story = { export const WithError: Story = {
args: { args: {
prompts: samplePrompts, prompts: samplePrompts,
selectedPromptName: "translate",
getPromptState: { getPromptState: {
status: "error", status: "error",
error: error:
'Prompt "translate" requires argument "text" but none was provided. Please fill in all required arguments before submitting.', 'Prompt "translate" requires argument "text" but none was provided. Please fill in all required arguments before submitting.',
}, },
}, },
play: async ({ canvasElement }) => {
await selectPromptByName(canvasElement, "translate");
},
}; };
export const ListChanged: Story = { export const ListChanged: Story = {

View file

@ -25,11 +25,9 @@ export interface GetPromptState {
export interface PromptsScreenProps { export interface PromptsScreenProps {
prompts: Prompt[]; prompts: Prompt[];
selectedPromptName?: string;
getPromptState?: GetPromptState; getPromptState?: GetPromptState;
listChanged: boolean; listChanged: boolean;
onRefreshList: () => void; onRefreshList: () => void;
onSelectPrompt: (name: string) => void;
onGetPrompt: (name: string, args: Record<string, string>) => void; onGetPrompt: (name: string, args: Record<string, string>) => void;
onCopyMessages?: () => void; onCopyMessages?: () => void;
} }
@ -65,14 +63,15 @@ const EmptyState = Text.withProps({
export function PromptsScreen({ export function PromptsScreen({
prompts, prompts,
selectedPromptName,
getPromptState, getPromptState,
listChanged, listChanged,
onRefreshList, onRefreshList,
onSelectPrompt,
onGetPrompt, onGetPrompt,
onCopyMessages, onCopyMessages,
}: PromptsScreenProps) { }: PromptsScreenProps) {
const [selectedPromptName, setSelectedPromptName] = useState<
string | undefined
>(undefined);
const [argumentValues, setArgumentValues] = useState<Record<string, string>>( const [argumentValues, setArgumentValues] = useState<Record<string, string>>(
{}, {},
); );
@ -91,7 +90,7 @@ export function PromptsScreen({
onRefreshList={onRefreshList} onRefreshList={onRefreshList}
onSelectPrompt={(name) => { onSelectPrompt={(name) => {
setArgumentValues({}); setArgumentValues({});
onSelectPrompt(name); setSelectedPromptName(name);
}} }}
/> />
</SidebarCard> </SidebarCard>

View file

@ -4,7 +4,7 @@ import type {
ResourceTemplate, ResourceTemplate,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js"; import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js";
import { fn } from "storybook/test"; import { fn, userEvent, within } from "storybook/test";
import { ResourcesScreen } from "./ResourcesScreen"; import { ResourcesScreen } from "./ResourcesScreen";
import type { ReadResourceState } from "./ResourcesScreen"; import type { ReadResourceState } from "./ResourcesScreen";
@ -14,8 +14,6 @@ const meta: Meta<typeof ResourcesScreen> = {
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
args: { args: {
onRefreshList: fn(), onRefreshList: fn(),
onSelectUri: fn(),
onSelectTemplate: fn(),
onReadResource: fn(), onReadResource: fn(),
onSubscribeResource: fn(), onSubscribeResource: fn(),
onUnsubscribeResource: fn(), onUnsubscribeResource: fn(),
@ -105,6 +103,40 @@ const readConfigState: ReadResourceState = {
isSubscribed: true, isSubscribed: true,
}; };
const readUserProfileState: ReadResourceState = {
status: "ok",
uri: "file:///users/42/profile",
result: {
contents: [
{
uri: "file:///users/42/profile",
mimeType: "application/json",
text: JSON.stringify(
{ id: 42, name: "Alice", email: "alice@example.com" },
null,
2,
),
},
],
},
isSubscribed: false,
};
async function clickByText(canvasElement: HTMLElement, label: string) {
const canvas = within(canvasElement);
await userEvent.click(await canvas.findByText(label));
}
async function expandUserProfileTemplate(canvasElement: HTMLElement) {
const canvas = within(canvasElement);
await userEvent.click(await canvas.findByText("User Profile"));
const userIdInput = await canvas.findByLabelText("userId");
await userEvent.type(userIdInput, "42");
await userEvent.click(
await canvas.findByRole("button", { name: "Read Resource" }),
);
}
export const WithResources: Story = { export const WithResources: Story = {
args: { args: {
resources: sampleResources, resources: sampleResources,
@ -115,9 +147,11 @@ export const ResourceSelected: Story = {
args: { args: {
resources: sampleResources, resources: sampleResources,
subscriptions: sampleSubscriptions, subscriptions: sampleSubscriptions,
selectedResourceUri: "file:///config.json",
readState: readConfigState, readState: readConfigState,
}, },
play: async ({ canvasElement }) => {
await clickByText(canvasElement, "config.json");
},
}; };
export const WithTemplates: Story = { export const WithTemplates: Story = {
@ -131,7 +165,9 @@ export const TemplateSelected: Story = {
args: { args: {
resources: sampleResources, resources: sampleResources,
templates: sampleTemplates, templates: sampleTemplates,
selectedTemplateUri: "file:///users/{userId}/profile", },
play: async ({ canvasElement }) => {
await clickByText(canvasElement, "User Profile");
}, },
}; };
@ -139,26 +175,10 @@ export const TemplateWithResource: Story = {
args: { args: {
resources: sampleResources, resources: sampleResources,
templates: sampleTemplates, templates: sampleTemplates,
selectedTemplateUri: "file:///users/{userId}/profile", readState: readUserProfileState,
readState: { },
status: "ok", play: async ({ canvasElement }) => {
uri: "file:///users/42/profile", await expandUserProfileTemplate(canvasElement);
result: {
contents: [
{
uri: "file:///users/42/profile",
mimeType: "application/json",
text: JSON.stringify(
{ id: 42, name: "Alice", email: "alice@example.com" },
null,
2,
),
},
],
},
isSubscribed: false,
},
selectedResourceUri: "file:///users/42/profile",
}, },
}; };
@ -167,26 +187,10 @@ export const AllSections: Story = {
resources: sampleResources, resources: sampleResources,
templates: sampleTemplates, templates: sampleTemplates,
subscriptions: sampleSubscriptions, subscriptions: sampleSubscriptions,
selectedTemplateUri: "file:///users/{userId}/profile", readState: readUserProfileState,
readState: { },
status: "ok", play: async ({ canvasElement }) => {
uri: "file:///users/42/profile", await expandUserProfileTemplate(canvasElement);
result: {
contents: [
{
uri: "file:///users/42/profile",
mimeType: "application/json",
text: JSON.stringify(
{ id: 42, name: "Alice", email: "alice@example.com" },
null,
2,
),
},
],
},
isSubscribed: false,
},
selectedResourceUri: "file:///users/42/profile",
}, },
}; };

View file

@ -1,3 +1,4 @@
import { useState } from "react";
import { import {
Alert, Alert,
Card, Card,
@ -31,13 +32,9 @@ export interface ResourcesScreenProps {
resources: Resource[]; resources: Resource[];
templates: ResourceTemplate[]; templates: ResourceTemplate[];
subscriptions: InspectorResourceSubscription[]; subscriptions: InspectorResourceSubscription[];
selectedResourceUri?: string;
selectedTemplateUri?: string;
readState?: ReadResourceState; readState?: ReadResourceState;
listChanged: boolean; listChanged: boolean;
onRefreshList: () => void; onRefreshList: () => void;
onSelectUri: (uri: string) => void;
onSelectTemplate: (uriTemplate: string) => void;
onReadResource: (uri: string) => void; onReadResource: (uri: string) => void;
onSubscribeResource: (uri: string) => void; onSubscribeResource: (uri: string) => void;
onUnsubscribeResource: (uri: string) => void; onUnsubscribeResource: (uri: string) => void;
@ -78,17 +75,20 @@ export function ResourcesScreen({
resources, resources,
templates, templates,
subscriptions, subscriptions,
selectedResourceUri,
selectedTemplateUri,
readState, readState,
listChanged, listChanged,
onRefreshList, onRefreshList,
onSelectUri,
onSelectTemplate,
onReadResource, onReadResource,
onSubscribeResource, onSubscribeResource,
onUnsubscribeResource, onUnsubscribeResource,
}: ResourcesScreenProps) { }: ResourcesScreenProps) {
const [selectedResourceUri, setSelectedResourceUri] = useState<
string | undefined
>(undefined);
const [selectedTemplateUri, setSelectedTemplateUri] = useState<
string | undefined
>(undefined);
const selectedResource = selectedResourceUri const selectedResource = selectedResourceUri
? resources.find((r) => r.uri === selectedResourceUri) ? resources.find((r) => r.uri === selectedResourceUri)
: undefined; : undefined;
@ -104,6 +104,11 @@ export function ResourcesScreen({
? { name: readState.uri, uri: readState.uri } ? { name: readState.uri, uri: readState.uri }
: undefined); : undefined);
function handleReadResource(uri: string) {
setSelectedResourceUri(uri);
onReadResource(uri);
}
function renderReadState() { function renderReadState() {
if (!readState) return null; if (!readState) return null;
@ -136,7 +141,7 @@ export function ResourcesScreen({
contents={readState.result.contents} contents={readState.result.contents}
lastUpdated={readState.lastUpdated} lastUpdated={readState.lastUpdated}
isSubscribed={readState.isSubscribed ?? false} isSubscribed={readState.isSubscribed ?? false}
onRefresh={() => onReadResource(readResource.uri)} onRefresh={() => handleReadResource(readResource.uri)}
onSubscribe={() => onSubscribeResource(readResource.uri)} onSubscribe={() => onSubscribeResource(readResource.uri)}
onUnsubscribe={() => onUnsubscribeResource(readResource.uri)} onUnsubscribe={() => onUnsubscribeResource(readResource.uri)}
/> />
@ -159,8 +164,8 @@ export function ResourcesScreen({
selectedTemplateUri={selectedTemplateUri} selectedTemplateUri={selectedTemplateUri}
listChanged={listChanged} listChanged={listChanged}
onRefreshList={onRefreshList} onRefreshList={onRefreshList}
onSelectUri={onSelectUri} onSelectUri={setSelectedResourceUri}
onSelectTemplate={onSelectTemplate} onSelectTemplate={setSelectedTemplateUri}
onUnsubscribeResource={onUnsubscribeResource} onUnsubscribeResource={onUnsubscribeResource}
/> />
</SidebarCard> </SidebarCard>
@ -172,7 +177,7 @@ export function ResourcesScreen({
<DetailCard> <DetailCard>
<ResourceTemplatePanel <ResourceTemplatePanel
template={selectedTemplate} template={selectedTemplate}
onReadResource={onReadResource} onReadResource={handleReadResource}
/> />
</DetailCard> </DetailCard>
</ScrollArea.Autosize> </ScrollArea.Autosize>

View file

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import type { ElicitRequest, Tool } from "@modelcontextprotocol/sdk/types.js"; import type { ElicitRequest, Tool } from "@modelcontextprotocol/sdk/types.js";
import { Modal } from "@mantine/core"; import { Modal } from "@mantine/core";
import { fn } from "storybook/test"; import { fn, userEvent, within } from "storybook/test";
import { ToolsScreen } from "./ToolsScreen"; import { ToolsScreen } from "./ToolsScreen";
import type { ToolCallState } from "./ToolsScreen"; import type { ToolCallState } from "./ToolsScreen";
import { SamplingRequestPanel } from "../../groups/SamplingRequestPanel/SamplingRequestPanel"; import { SamplingRequestPanel } from "../../groups/SamplingRequestPanel/SamplingRequestPanel";
@ -17,7 +17,6 @@ const meta: Meta<typeof ToolsScreen> = {
args: { args: {
listChanged: false, listChanged: false,
onRefreshList: fn(), onRefreshList: fn(),
onSelectTool: fn(),
onCallTool: fn(), onCallTool: fn(),
onCancelCall: fn(), onCancelCall: fn(),
onClearResult: fn(), onClearResult: fn(),
@ -77,6 +76,11 @@ const resultState: ToolCallState = {
}, },
}; };
async function selectToolByLabel(canvasElement: HTMLElement, label: string) {
const canvas = within(canvasElement);
await userEvent.click(await canvas.findByText(label));
}
export const NoSelection: Story = { export const NoSelection: Story = {
args: { args: {
tools: sampleTools, tools: sampleTools,
@ -86,16 +90,20 @@ export const NoSelection: Story = {
export const ToolSelected: Story = { export const ToolSelected: Story = {
args: { args: {
tools: sampleTools, tools: sampleTools,
selectedToolName: "create_record", },
play: async ({ canvasElement }) => {
await selectToolByLabel(canvasElement, "Create Record");
}, },
}; };
export const WithResult: Story = { export const WithResult: Story = {
args: { args: {
tools: sampleTools, tools: sampleTools,
selectedToolName: "create_record",
callState: resultState, callState: resultState,
}, },
play: async ({ canvasElement }) => {
await selectToolByLabel(canvasElement, "Create Record");
},
}; };
export const LongToolName: Story = { export const LongToolName: Story = {
@ -130,15 +138,18 @@ export const LongToolName: Story = {
}, },
}, },
], ],
selectedToolName: },
play: async ({ canvasElement }) => {
await selectToolByLabel(
canvasElement,
"organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering", "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
);
}, },
}; };
export const WithError: Story = { export const WithError: Story = {
args: { args: {
tools: sampleTools, tools: sampleTools,
selectedToolName: "create_record",
callState: { callState: {
status: "error", status: "error",
result: { result: {
@ -152,6 +163,9 @@ export const WithError: Story = {
}, },
}, },
}, },
play: async ({ canvasElement }) => {
await selectToolByLabel(canvasElement, "Create Record");
},
}; };
export const WithListChanged: Story = { export const WithListChanged: Story = {
@ -176,9 +190,11 @@ const elicitFormRequest = {
export const WithSamplingModal: Story = { export const WithSamplingModal: Story = {
args: { args: {
tools: sampleTools, tools: sampleTools,
selectedToolName: "create_record",
callState: { status: "pending" }, callState: { status: "pending" },
}, },
play: async ({ canvasElement }) => {
await selectToolByLabel(canvasElement, "Create Record");
},
render: (args) => ( render: (args) => (
<> <>
<ToolsScreen {...args} /> <ToolsScreen {...args} />
@ -217,9 +233,11 @@ export const WithSamplingModal: Story = {
export const WithElicitationModal: Story = { export const WithElicitationModal: Story = {
args: { args: {
tools: sampleTools, tools: sampleTools,
selectedToolName: "create_record",
callState: { status: "pending" }, callState: { status: "pending" },
}, },
play: async ({ canvasElement }) => {
await selectToolByLabel(canvasElement, "Create Record");
},
render: (args) => ( render: (args) => (
<> <>
<ToolsScreen {...args} /> <ToolsScreen {...args} />
@ -240,9 +258,11 @@ export const WithElicitationModal: Story = {
export const WithPendingRequests: Story = { export const WithPendingRequests: Story = {
args: { args: {
tools: sampleTools, tools: sampleTools,
selectedToolName: "create_record",
callState: { status: "pending" }, callState: { status: "pending" },
}, },
play: async ({ canvasElement }) => {
await selectToolByLabel(canvasElement, "Create Record");
},
render: (args) => ( render: (args) => (
<> <>
<ToolsScreen {...args} /> <ToolsScreen {...args} />

View file

@ -17,11 +17,9 @@ export interface ToolCallState {
export interface ToolsScreenProps { export interface ToolsScreenProps {
tools: Tool[]; tools: Tool[];
selectedToolName?: string;
callState?: ToolCallState; callState?: ToolCallState;
listChanged: boolean; listChanged: boolean;
onRefreshList: () => void; onRefreshList: () => void;
onSelectTool: (name: string) => void;
onCallTool: (name: string, args: Record<string, unknown>) => void; onCallTool: (name: string, args: Record<string, unknown>) => void;
onCancelCall?: () => void; onCancelCall?: () => void;
onClearResult?: () => void; onClearResult?: () => void;
@ -58,15 +56,16 @@ const EmptyState = Text.withProps({
export function ToolsScreen({ export function ToolsScreen({
tools, tools,
selectedToolName,
callState, callState,
listChanged, listChanged,
onRefreshList, onRefreshList,
onSelectTool,
onCallTool, onCallTool,
onCancelCall, onCancelCall,
onClearResult, onClearResult,
}: ToolsScreenProps) { }: ToolsScreenProps) {
const [selectedToolName, setSelectedToolName] = useState<string | undefined>(
undefined,
);
const [formValues, setFormValues] = useState<Record<string, unknown>>({}); const [formValues, setFormValues] = useState<Record<string, unknown>>({});
const selectedTool = selectedToolName const selectedTool = selectedToolName
? tools.find((t) => t.name === selectedToolName) ? tools.find((t) => t.name === selectedToolName)
@ -84,7 +83,7 @@ export function ToolsScreen({
onRefreshList={onRefreshList} onRefreshList={onRefreshList}
onSelectTool={(name) => { onSelectTool={(name) => {
setFormValues({}); setFormValues({});
onSelectTool(name); setSelectedToolName(name);
}} }}
/> />
</SidebarCard> </SidebarCard>

View file

@ -289,7 +289,6 @@ export function InspectorView({
tools={tools} tools={tools}
listChanged={false} listChanged={false}
onRefreshList={noop} onRefreshList={noop}
onSelectTool={noop}
onCallTool={noop} onCallTool={noop}
/> />
</ScreenStage> </ScreenStage>
@ -298,7 +297,6 @@ export function InspectorView({
prompts={prompts} prompts={prompts}
listChanged={false} listChanged={false}
onRefreshList={noop} onRefreshList={noop}
onSelectPrompt={noop}
onGetPrompt={noop} onGetPrompt={noop}
/> />
</ScreenStage> </ScreenStage>
@ -309,8 +307,6 @@ export function InspectorView({
subscriptions={subscriptions} subscriptions={subscriptions}
listChanged={false} listChanged={false}
onRefreshList={noop} onRefreshList={noop}
onSelectUri={noop}
onSelectTemplate={noop}
onReadResource={noop} onReadResource={noop}
onSubscribeResource={noop} onSubscribeResource={noop}
onUnsubscribeResource={noop} onUnsubscribeResource={noop}

View file

@ -532,6 +532,56 @@ existing stories) rather than a screen refactor.
- No prop changes needed. - No prop changes needed.
- Optional: add `connectionStatus` if the embedded header needs it. - Optional: add `connectionStatus` if the embedded header needs it.
### 4.3 Move selection state into the screens
The Phase 2 cluster work threaded `selectedXxxName` / `onSelectXxx` props
through every list-and-detail screen, leaving selection as caller-owned
state. That choice contradicts Phase 3's "selected name stays in `useState`
inside the screen" rule and creates a real wiring gap: nothing above the
screen has any reason to know which tool/prompt/resource the user clicked
on, so under `InspectorView` clicks would have to be round-tripped through
`App.tsx` purely to get the detail panel to render.
Selection is screen-local UI state. MCP-derived data (`callState` / fetched
prompt messages / resource read results) keeps flowing in from
`InspectorView` as props. Refactor each screen accordingly.
Screens to fix:
- **`ToolsScreen`** — drop `selectedToolName` and `onSelectTool` from the
prop interface; track the selected tool name in `useState` inside the
screen. `ToolControls` continues to receive `selectedName` and
`onSelectTool` as props from the screen. `callState` (and any future
result/progress payload) keeps coming in from `InspectorView`.
`InspectorView` only needs to know the selected tool name when it
triggers `onCallTool` — which the screen already owns, so it can pass
the name up at call time rather than maintaining it in the parent.
- **`PromptsScreen`** — same pattern for `selectedPromptName`. Fetched
`GetPromptResult` continues to arrive as a prop from `InspectorView`.
- **`ResourcesScreen`** — same pattern for the selected resource URI and
selected resource template URI. `ReadResourceResult` continues to
arrive as a prop.
For each screen:
1. Remove the `selectedXxxName` / `onSelectXxx` props from the screen's
prop interface.
2. Add a `useState` for the selection inside the screen and pass it down
to the controls group.
3. When the user triggers a request (`onCallTool` / `onGetPrompt` /
`onReadResource`), pass the currently selected name/uri up alongside
any other arguments — the parent does not need to track selection
between calls.
4. Update each screen's stories to remove the now-deleted props. The
stories will start working without `useArgs` selection wiring, because
selection is now genuinely internal.
5. Audit the controls groups (`ToolControls`, `PromptControls`,
`ResourceControls`) — their `selectedName` / `onSelectXxx` props are
correct (they ARE caller-owned, because the screen owns them) and
should not change.
6. Update `InspectorView` and any wiring fixtures to drop the now-removed
selection props they were passing to each screen.
## Phase 5 — Cleanup pass ## Phase 5 — Cleanup pass
After all four phases, sweep: After all four phases, sweep: